From 9c8bfd4139df6a555ad6b9e23ff009d1c399490c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Apr 2026 15:25:25 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../components/fields/entity/SearchBarUM.kt | 1 + .../ui/ds/field/search/TangemSearchField.kt | 9 +- .../ui/ds/opportunities/OpportunitiesBG.kt | 4 +- .../domain/search/model/SearchResult.kt | 2 +- .../search/model/UserAssetSearchEntry.kt | 2 + .../search/model/UserAssetSearchItem.kt | 13 ++ .../search/usecase/GetSearchResultsUseCase.kt | 46 ++++++- .../feed/components/FeedEntryChildFactory.kt | 7 + .../search/DefaultSearchComponent.kt | 4 + .../features/feed/model/search/SearchModel.kt | 87 +++++++----- .../converter/UserAssetSearchItemConverter.kt | 92 +++++++++++++ .../features/feed/ui/search/SearchContent.kt | 125 +++++++++++++++--- .../ui/search/preview/SearchContentPreview.kt | 40 +++--- .../feed/ui/search/state/SearchCallbacks.kt | 1 + .../features/feed/ui/search/state/SearchUM.kt | 53 ++++++-- 15 files changed, 394 insertions(+), 92 deletions(-) create mode 100644 domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt index 158c768fe8..6050d96cb3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt @@ -9,4 +9,5 @@ data class SearchBarUM( val isActive: Boolean, val onActiveChange: (Boolean) -> Unit, val onClearClick: () -> Unit = {}, + val onCancelClick: (() -> Unit)? = null, ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt index 4e2e9ebb42..905ef8c5fb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt @@ -289,8 +289,13 @@ private fun CancelButton( state.onQueryChange("") } keyboardController?.hide() - state.onClearClick() - focusManager.clearFocus() + val onCancel = state.onCancelClick + if (onCancel != null) { + onCancel() + } else { + state.onClearClick() + focusManager.clearFocus() + } }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt index 79f093e351..ac427c9961 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt @@ -193,7 +193,7 @@ private fun BoxScope.UrlBackground(imageUrl: String?, blurRadius: Dp) { AsyncImage( model = imageRequest, contentDescription = null, - contentScale = ContentScale.Crop, + contentScale = ContentScale.FillBounds, modifier = Modifier .matchParentSize() .scale(SCALE_FACTOR) @@ -207,7 +207,7 @@ private fun BoxScope.ResBackground(res: Int, blurRadius: Dp) { Image( painter = painterResource(res), contentDescription = null, - contentScale = ContentScale.Crop, + contentScale = ContentScale.FillBounds, modifier = Modifier .matchParentSize() .scale(SCALE_FACTOR) diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt b/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt index 98a25e4996..40fc76fed4 100644 --- a/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt +++ b/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt @@ -3,5 +3,5 @@ package com.tangem.domain.search.model data class SearchResult( val textHints: List, val recentTokens: List, - val userAssets: List, + val userAssets: List, ) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt index fd64a23e24..c6a3d45854 100644 --- a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt +++ b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt @@ -2,6 +2,7 @@ package com.tangem.domain.search.model import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId @@ -10,5 +11,6 @@ data class UserAssetSearchEntry( val userWalletName: String, val accountId: AccountId, val accountName: AccountName, + val accountIcon: CryptoPortfolioIcon, val currencyStatus: CryptoCurrencyStatus, ) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt new file mode 100644 index 0000000000..7a63289486 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.search.model + +sealed interface UserAssetSearchItem { + + data class Single(val entry: UserAssetSearchEntry) : UserAssetSearchItem + + data class Grouped( + val tokenName: String, + val tokenSymbol: String, + val tokenIconUrl: String?, + val entries: List, + ) : UserAssetSearchItem +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt index b6167208a2..0b6b37be48 100644 --- a/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt @@ -9,10 +9,12 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.search.model.SearchResult import com.tangem.domain.search.model.UserAssetSearchEntry +import com.tangem.domain.search.model.UserAssetSearchItem import com.tangem.domain.search.repository.SearchRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import java.math.BigDecimal /** * Primary search use case that produces [SearchResult] based on the current query. @@ -70,9 +72,12 @@ class GetSearchResultsUseCase( if (unlockedWallets.isEmpty()) return@combine emptyList() - statusLists + val entries = statusLists .filter { it.userWalletId in unlockedWallets } .flatMap { statusList -> extractMatchingAssets(statusList, unlockedWallets, lowerQuery) } + + val shouldGroup = needsGrouping(unlockedWallets.values, statusLists) + groupAndSort(entries, shouldGroup) }.map { userAssets -> SearchResult( textHints = emptyList(), @@ -82,6 +87,44 @@ class GetSearchResultsUseCase( } } + private fun needsGrouping(unlockedWallets: Collection, statusLists: List): Boolean { + if (unlockedWallets.size > 1) return true + + val totalAccounts = statusLists + .filter { sl -> unlockedWallets.any { it.walletId == sl.userWalletId } } + .sumOf { it.accountStatuses.filterCryptoPortfolio().size } + + return totalAccounts > 1 + } + + private fun groupAndSort(entries: List, shouldGroup: Boolean): List { + if (!shouldGroup) { + return entries + .map { UserAssetSearchItem.Single(it) } + .sortedByDescending { it.entry.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO } + } + + val grouped = entries.groupBy { entry -> + val rawId = entry.currencyStatus.currency.id.rawCurrencyId + rawId?.value ?: "${entry.currencyStatus.currency.name}|${entry.currencyStatus.currency.symbol}" + } + + return grouped.map { (_, groupEntries) -> + val assetInfo = groupEntries.first() + UserAssetSearchItem.Grouped( + tokenName = assetInfo.currencyStatus.currency.name, + tokenSymbol = assetInfo.currencyStatus.currency.symbol, + tokenIconUrl = assetInfo.currencyStatus.currency.iconUrl, + entries = groupEntries, + ) + }.sortedByDescending { item -> + when (item) { + is UserAssetSearchItem.Grouped -> + item.entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO } + } + } + } + private fun extractMatchingAssets( statusList: AccountStatusList, wallets: Map, @@ -103,6 +146,7 @@ class GetSearchResultsUseCase( userWalletName = wallet.name, accountId = accountStatus.accountId, accountName = accountStatus.account.accountName, + accountIcon = accountStatus.account.icon, currencyStatus = currencyStatus, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index a51d8689f7..fb93121de6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -148,6 +148,13 @@ internal class FeedEntryChildFactory @Inject constructor( appComponentContext = appComponentContext, params = DefaultSearchComponent.Params( onBackClick = onBackClicked, + onMarketTokenClick = { token, currency -> + feedEntryClickIntents.onMarketItemClick( + token = token, + appCurrency = currency, + source = AnalyticsParam.ScreensSources.Market.value, + ) + }, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt index b8a8ac1119..677f40ed5a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt @@ -15,6 +15,8 @@ import com.tangem.core.ui.ds.field.search.TangemFieldShape import com.tangem.core.ui.ds.field.search.TangemSearchField import com.tangem.core.ui.ds.topbar.TangemTopBar 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.search.SearchContent import com.tangem.features.feed.ui.search.state.SearchCallbacks @@ -74,6 +76,7 @@ internal class DefaultSearchComponent( onClearHintsClick = model::clearSearchHistory, onTextHintClick = model::onTextHintClick, onResultMarketTokenClick = model::onResultMarketTokenClick, + onHistoryTokenClick = model::onHistoryTokenClick, ) } SearchContent( @@ -86,5 +89,6 @@ internal class DefaultSearchComponent( data class Params( val onBackClick: () -> Unit, + val onMarketTokenClick: ((TokenMarketParams, AppCurrency) -> Unit), ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt index 2273f437bb..f43e8c5766 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt @@ -1,19 +1,21 @@ package com.tangem.features.feed.model.search import arrow.core.getOrElse +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter +import com.tangem.common.ui.charts.state.sorted import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.common.ui.charts.state.MarketChartData -import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter -import com.tangem.common.ui.charts.state.sorted +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.markets.GetTokenPriceChartUseCase import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.models.account.AccountName +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase import com.tangem.domain.search.usecase.GetSearchResultsUseCase import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase @@ -26,6 +28,7 @@ import com.tangem.features.feed.model.search.converter.MarketsListItemUMToRecent import com.tangem.features.feed.model.search.converter.MarketsListItemUMWithAppCurrency import com.tangem.features.feed.model.search.converter.RecentSearchTokenToMarketsListItemUMConverter import com.tangem.features.feed.model.search.converter.RecentSearchTokenWithAppCurrency +import com.tangem.features.feed.model.search.converter.UserAssetSearchItemConverter import com.tangem.features.feed.model.search.state.SearchStateController import com.tangem.features.feed.model.search.state.transformers.* import com.tangem.features.feed.ui.search.state.* @@ -35,13 +38,8 @@ import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import javax.inject.Inject private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L @@ -54,6 +52,7 @@ internal class SearchModel @Inject constructor( paramsContainer: ParamsContainer, getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getSearchResultsUseCase: GetSearchResultsUseCase, private val saveSearchQueryUseCase: SaveSearchQueryUseCase, private val saveRecentSearchTokenUseCase: SaveRecentSearchTokenUseCase, @@ -77,6 +76,13 @@ internal class SearchModel @Inject constructor( initialValue = AppCurrency.Default, ) + private val isBalanceHidden = getBalanceHidingSettingsUseCase.isBalanceHidden() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + private val marketsListItemToRecentSearchTokenConverter by lazy { MarketsListItemUMToRecentSearchTokenConverter() } @@ -143,17 +149,38 @@ internal class SearchModel @Inject constructor( ) saveRecentSearchTokenUseCase(marketsListItemToRecentSearchTokenConverter.convert(input)) saveSearchQueryUseCase(stateController.value.searchBar.query) + withContext(dispatchers.mainImmediate) { + searchMarketsListManager.getTokenById(item.id)?.let { found -> + params.onMarketTokenClick(found.toSerializableParam(), appCurrency) + } + } } } + fun onHistoryTokenClick(item: MarketsListItemUM) { + val tokenMarketParams = TokenMarketParams( + id = item.id, + name = item.name, + symbol = item.currencySymbol, + tokenQuotes = TokenMarketParams.Quotes( + currentPrice = item.price.fiatPrice, + h24Percent = null, + weekPercent = null, + monthPercent = null, + ), + imageUrl = item.iconUrl, + ) + params.onMarketTokenClick(tokenMarketParams, currentAppCurrency.value) + } + private fun initCallbacks() { stateController.update(object : SearchUMTransformer { override fun transform(prevState: SearchUM): SearchUM { return prevState.copy( searchBar = prevState.searchBar.copy( onQueryChange = ::onQueryChange, - onActiveChange = ::onActiveChange, onClearClick = ::onClearClick, + onCancelClick = params.onBackClick, ), ) } @@ -164,10 +191,6 @@ internal class SearchModel @Inject constructor( stateController.update(UpdateSearchBarQueryTransformer(query)) } - private fun onActiveChange(isActive: Boolean) { - if (!isActive) params.onBackClick() - } - private fun onClearClick() { stateController.update(UpdateSearchBarQueryTransformer("")) } @@ -200,20 +223,19 @@ internal class SearchModel @Inject constructor( private fun subscribeToSearchResults(query: String) { modelScope.launch { - getSearchResultsUseCase(query = query).collectLatest { searchResult -> - val userAssets = searchResult.userAssets.map { entry -> - UserAssetItemUM( - id = "${entry.userWalletId.stringValue}_${entry.accountId.value}" + - "_${entry.currencyStatus.currency.id.value}", - tokenIconUrl = entry.currencyStatus.currency.iconUrl, - tokenName = entry.currencyStatus.currency.name, - tokenSymbol = entry.currencyStatus.currency.symbol, - accountName = entry.accountName.toDisplayString(), - onClick = { - // TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task. - }, - ) - }.toImmutableList() + combine( + getSearchResultsUseCase(query = query), + currentAppCurrency, + isBalanceHidden, + ) { searchResult, appCurrency, balanceHidden -> + val converter = UserAssetSearchItemConverter( + appCurrency = appCurrency, + isBalanceHidden = balanceHidden, + ) + searchResult.userAssets + .map(converter::convert) + .toImmutableList() + }.collectLatest { userAssets -> stateController.update(UpdateUserAssetsTransformer(userAssets)) } }.saveIn(searchResultsJob) @@ -334,13 +356,6 @@ internal class SearchModel @Inject constructor( } } - private fun AccountName.toDisplayString(): String { - return when (this) { - is AccountName.DefaultMain -> "Main" // TODO [REDACTED_TASK_KEY] localize - is AccountName.Custom -> value - } - } - private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) { launch { while (true) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt new file mode 100644 index 0000000000..4e742afdde --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt @@ -0,0 +1,92 @@ +package com.tangem.features.feed.model.search.converter + +import com.tangem.common.ui.account.toUM +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.search.model.UserAssetSearchEntry +import com.tangem.domain.search.model.UserAssetSearchItem +import com.tangem.features.feed.ui.search.state.UserAssetItemUM +import com.tangem.utils.StringsSigns +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +internal class UserAssetSearchItemConverter( + private val appCurrency: AppCurrency, + private val isBalanceHidden: Boolean, +) : Converter { + + override fun convert(value: UserAssetSearchItem): UserAssetItemUM { + return when (value) { + is UserAssetSearchItem.Single -> convertSingle(value.entry) + is UserAssetSearchItem.Grouped -> convertGrouped(value) + } + } + + private fun convertSingle(entry: UserAssetSearchEntry): UserAssetItemUM.Single { + val currency = entry.currencyStatus.currency + val value = entry.currencyStatus.value + + return UserAssetItemUM.Single( + id = "${entry.userWalletId.stringValue}_${entry.accountId.value}_${currency.id.value}", + icon = TangemIconUM.Currency( + currencyIconState = CryptoCurrencyToIconStateConverter().convert(entry.currencyStatus), + ), + tokenName = currency.name, + tokenSymbol = currency.symbol, + fiatRate = value.fiatRate?.format { fiat(appCurrency.code, appCurrency.symbol) }, + cryptoBalance = formatCryptoAmount(value.amount, currency.symbol, currency.decimals), + fiatBalance = formatFiatAmount(value.fiatAmount), + isBalanceHidden = isBalanceHidden, + onClick = {}, + ) + } + + private fun convertGrouped(item: UserAssetSearchItem.Grouped): UserAssetItemUM.Grouped { + val totalFiat = item.entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO } + val totalCrypto = item.entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO } + + val firstCurrency = item.entries.first().currencyStatus.currency + val children = item.entries.map { entry -> + UserAssetItemUM.GroupedChild( + walletName = entry.userWalletName, + accountName = entry.accountName.toUM(), + accountIcon = entry.accountIcon.value, + accountColor = entry.accountIcon.color, + cryptoBalance = formatCryptoAmount( + entry.currencyStatus.value.amount, + entry.currencyStatus.currency.symbol, + entry.currencyStatus.currency.decimals, + ), + fiatBalance = formatFiatAmount(entry.currencyStatus.value.fiatAmount), + ) + }.toImmutableList() + + return UserAssetItemUM.Grouped( + id = "grouped_${item.tokenName}_${item.tokenSymbol}", + icon = TangemIconUM.Currency( + currencyIconState = CryptoCurrencyToIconStateConverter().convert(item.entries.first().currencyStatus), + ), + tokenName = item.tokenName, + tokenSymbol = item.tokenSymbol, + tokensCount = item.entries.size, + totalCryptoBalance = formatCryptoAmount(totalCrypto, firstCurrency.symbol, firstCurrency.decimals), + totalFiatBalance = formatFiatAmount(totalFiat), + isBalanceHidden = isBalanceHidden, + children = children, + onClick = {}, + ) + } + + private fun formatCryptoAmount(amount: BigDecimal?, symbol: String, decimals: Int): String { + return amount?.format { crypto(symbol, decimals) } ?: StringsSigns.DASH_SIGN + } + + private fun formatFiatAmount(fiatAmount: BigDecimal?): String { + return fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) } ?: StringsSigns.DASH_SIGN + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt index 287b580b15..6d7118f812 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt @@ -3,20 +3,16 @@ package com.tangem.features.feed.ui.search import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect 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 @@ -32,7 +28,6 @@ import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.ds.button.* import com.tangem.core.ui.ds.image.TangemIcon -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 @@ -54,6 +49,15 @@ internal fun SearchContent( val lazyListState = rememberLazyListState() val background = LocalMainBottomSheetColor.current.value + val contentStructureKey = when (content) { + is SearchContentUM.InitialEmpty -> "empty" + is SearchContentUM.History -> "history" + is SearchContentUM.Results -> "results_${content.userAssets.isNotEmpty()}" + } + LaunchedEffect(contentStructureKey) { + lazyListState.scrollToItem(0) + } + LazyColumn( state = lazyListState, modifier = modifier @@ -72,6 +76,7 @@ internal fun SearchContent( history = content, onClearAllClick = searchCallbacks.onClearHintsClick, onHintClick = searchCallbacks.onTextHintClick, + onHistoryTokenClick = searchCallbacks.onHistoryTokenClick, ) is SearchContentUM.Results -> searchResultsItems( results = content, @@ -98,6 +103,7 @@ private fun LazyListScope.searchHistoryItems( history: SearchContentUM.History, onClearAllClick: (() -> Unit), onHintClick: (String) -> Unit, + onHistoryTokenClick: (MarketsListItemUM) -> Unit, ) { if (!history.textHints.isEmpty() || !history.recentTokens.isEmpty()) { item(key = "recents") { @@ -107,15 +113,20 @@ private fun LazyListScope.searchHistoryItems( ) } } - items( + itemsIndexed( items = history.textHints, - key = { "hint_${it.text}" }, - ) { hint -> + key = { _, item -> "hint_${item.text}" }, + ) { index, hint -> TextHintItem(hint = hint, onHintClick = { onHintClick(hint.text) }) - HorizontalDivider( - modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2), - color = TangemTheme.colors2.border.neutral.primary, - ) + if (index < history.textHints.size - 1) { + HorizontalDivider( + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2), + color = TangemTheme.colors2.border.neutral.primary, + ) + } + } + item { + SpacerH(TangemTheme.dimens2.x2) } items( items = history.recentTokens, @@ -129,7 +140,7 @@ private fun LazyListScope.searchHistoryItems( shape = RoundedCornerShape(TangemTheme.dimens2.x5), ), model = token, - onClick = {}, // TODO in [REDACTED_TASK_KEY] + onClick = { onHistoryTokenClick(token) }, ) } } @@ -264,9 +275,16 @@ private fun TextHintItem(hint: TextHintItemUM, onHintClick: () -> Unit) { } } -// TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task. @Composable private fun UserAssetItem(asset: UserAssetItemUM) { + when (asset) { + is UserAssetItemUM.Single -> SingleUserAssetItem(asset) + is UserAssetItemUM.Grouped -> GroupedUserAssetItem(asset) + } +} + +@Composable +private fun SingleUserAssetItem(asset: UserAssetItemUM.Single) { Row( modifier = Modifier .fillMaxWidth() @@ -276,10 +294,8 @@ private fun UserAssetItem(asset: UserAssetItemUM) { horizontalArrangement = Arrangement.spacedBy(8.dp), ) { TangemIcon( - tangemIconUM = TangemIconUM.Url(asset.tokenIconUrl, fallbackRes = R.drawable.ic_custom_token_44), - modifier = Modifier - .size(40.dp) - .clip(CircleShape), + modifier = Modifier.size(40.dp), + tangemIconUM = asset.icon, ) Column(modifier = Modifier.weight(1f)) { Text( @@ -289,12 +305,79 @@ private fun UserAssetItem(asset: UserAssetItemUM) { maxLines = 1, ) Text( - text = "${asset.tokenSymbol} · ${asset.accountName}", + text = asset.tokenSymbol, style = TangemTheme.typography2.captionRegular13, color = TangemTheme.colors2.text.neutral.tertiary, maxLines = 1, ) } + if (!asset.isBalanceHidden) { + Column(horizontalAlignment = Alignment.End) { + Text( + text = asset.fiatBalance, + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + ) + Text( + text = asset.cryptoBalance, + style = TangemTheme.typography2.captionRegular13, + color = TangemTheme.colors2.text.neutral.tertiary, + maxLines = 1, + ) + } + } + } +} + +// TODO [REDACTED_JIRA] update ui item to Portfolio block item +@Composable +private fun GroupedUserAssetItem(asset: UserAssetItemUM.Grouped) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = asset.onClick) + .padding(horizontal = 12.dp, vertical = 14.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + TangemIcon( + modifier = Modifier.size(40.dp), + tangemIconUM = asset.icon, + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = asset.tokenName, + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + ) + Text( + text = "${asset.tokenSymbol} · ${asset.tokensCount}", + style = TangemTheme.typography2.captionRegular13, + color = TangemTheme.colors2.text.neutral.tertiary, + maxLines = 1, + ) + } + if (!asset.isBalanceHidden) { + Column(horizontalAlignment = Alignment.End) { + Text( + text = asset.totalFiatBalance, + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + ) + Text( + text = asset.totalCryptoBalance, + style = TangemTheme.typography2.captionRegular13, + color = TangemTheme.colors2.text.neutral.tertiary, + maxLines = 1, + ) + } + } + } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt index bfb95e3819..e5cd68f79c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt @@ -2,11 +2,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 +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -15,7 +11,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -206,18 +204,22 @@ internal object SearchContentPreviewFixtures { updateTimestamp = updateTimestamp, ) - private fun userAsset( - id: String, - name: String, - symbol: String, - accountName: String, - iconUrl: String? = null, - ): UserAssetItemUM = UserAssetItemUM( + private fun userAsset(id: String, name: String, symbol: String): UserAssetItemUM = UserAssetItemUM.Single( id = id, - tokenIconUrl = iconUrl, + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = com.tangem.core.ui.R.drawable.ic_ethereumpow_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), tokenName = name, tokenSymbol = symbol, - accountName = accountName, + fiatRate = "$98,765.43", + cryptoBalance = "1.234 $symbol", + fiatBalance = "$121,876.50", + isBalanceHidden = false, onClick = {}, ) @@ -251,13 +253,8 @@ internal object SearchContentPreviewFixtures { ) private fun portfolioTwo(): ImmutableList = persistentListOf( - userAsset(id = "p1", name = "Ethereum", symbol = "ETH", accountName = "Main wallet"), - userAsset( - id = "p2", - name = "Polygon", - symbol = "POL", - accountName = "Account with a long label for preview", - ), + userAsset(id = "p1", name = "Ethereum", symbol = "ETH"), + userAsset(id = "p2", name = "Polygon", symbol = "POL"), ) private fun marketListShort(): ImmutableList = persistentListOf( @@ -390,6 +387,7 @@ private val SearchContentPreviewCallbacks = SearchCallbacks( onClearHintsClick = {}, onTextHintClick = { _ -> }, onResultMarketTokenClick = { _ -> }, + onHistoryTokenClick = { _ -> }, ) /** All [SearchContentPreviewScenario] values for the Preview Parameter dropdown in Android Studio. */ diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt index e5641b2bb8..9535331da7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt @@ -7,4 +7,5 @@ internal data class SearchCallbacks( val onClearHintsClick: () -> Unit, val onTextHintClick: (hint: String) -> Unit, val onResultMarketTokenClick: (MarketsListItemUM) -> Unit, + val onHistoryTokenClick: (MarketsListItemUM) -> Unit, ) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt index e4628ad30c..ede3decb1b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt @@ -1,8 +1,11 @@ package com.tangem.features.feed.ui.search.state import androidx.compose.runtime.Immutable +import com.tangem.common.ui.account.AccountNameUM import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.domain.models.account.CryptoPortfolioIcon import kotlinx.collections.immutable.ImmutableList data class SearchUM( @@ -42,11 +45,45 @@ sealed interface MarketSearchResultUM { data class TextHintItemUM(val text: String) -data class UserAssetItemUM( - val id: String, - val tokenIconUrl: String?, - val tokenName: String, - val tokenSymbol: String, - val accountName: String, - val onClick: () -> Unit, -) \ No newline at end of file +@Immutable +sealed interface UserAssetItemUM { + val id: String + val icon: TangemIconUM + val tokenName: String + val tokenSymbol: String + val onClick: () -> Unit + + data class Single( + override val id: String, + override val icon: TangemIconUM, + override val tokenName: String, + override val tokenSymbol: String, + val fiatRate: String?, + val cryptoBalance: String, + val fiatBalance: String, + val isBalanceHidden: Boolean, + override val onClick: () -> Unit, + ) : UserAssetItemUM + + data class Grouped( + override val id: String, + override val icon: TangemIconUM, + override val tokenName: String, + override val tokenSymbol: String, + val tokensCount: Int, + val totalCryptoBalance: String, + val totalFiatBalance: String, + val isBalanceHidden: Boolean, + val children: ImmutableList, + override val onClick: () -> Unit, + ) : UserAssetItemUM + + data class GroupedChild( + val walletName: String, + val accountName: AccountNameUM, + val accountIcon: CryptoPortfolioIcon.Icon, + val accountColor: CryptoPortfolioIcon.Color, + val cryptoBalance: String, + val fiatBalance: String, + ) +} \ No newline at end of file