diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5379cdff1d..bb907bd129 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -161,6 +161,7 @@ dependencies { implementation(projects.domain.news) implementation(projects.domain.earn) implementation(projects.domain.tokensync) + implementation(projects.domain.search) implementation(projects.common) implementation(projects.common.routing) @@ -216,6 +217,7 @@ dependencies { implementation(projects.data.hotWallet) implementation(projects.data.news) implementation(projects.data.earn) + implementation(projects.data.search) /** Features */ implementation(projects.features.referral.impl) diff --git a/data/search/.gitignore b/data/search/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/search/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/search/build.gradle.kts b/data/search/build.gradle.kts new file mode 100644 index 0000000000..96eb497c11 --- /dev/null +++ b/data/search/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.search" +} + +dependencies { + // region Project - Core + implementation(projects.core.datasource) + api(projects.core.utils) + // endregion + + // region Project - Data + implementation(projects.data.common) + // endregion + + // region Project - Domain + implementation(projects.domain.search) + implementation(projects.domain.common) + implementation(projects.domain.account.status) + implementation(projects.domain.markets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.appCurrency) + // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Other libraries + implementation(deps.androidx.datastore) + implementation(deps.moshi.kotlin) + // endregion +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt b/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt new file mode 100644 index 0000000000..60bedf12bd --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt @@ -0,0 +1,41 @@ +package com.tangem.data.search.converter + +import com.tangem.data.search.model.RecentTokenDTO +import com.tangem.data.search.model.TextHintDTO +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.model.SearchTextHint +import com.tangem.utils.converter.Converter + +internal class TextHintDTOToSearchTextHintConverter : Converter { + override fun convert(value: TextHintDTO): SearchTextHint { + return SearchTextHint( + text = value.text, + timestamp = value.timestamp, + ) + } +} + +internal class RecentTokenDTOToRecentSearchTokenConverter : Converter { + override fun convert(value: RecentTokenDTO): RecentSearchToken { + return RecentSearchToken( + id = CryptoCurrency.RawID(value.id), + name = value.name, + symbol = value.symbol, + imageUrl = value.imageUrl, + timestamp = value.timestamp, + ) + } +} + +internal class RecentSearchTokenToRecentTokenDTOConverter : Converter { + override fun convert(value: RecentSearchToken): RecentTokenDTO { + return RecentTokenDTO( + id = value.id.value, + name = value.name, + symbol = value.symbol, + imageUrl = value.imageUrl, + timestamp = value.timestamp, + ) + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/di/SearchDataModule.kt b/data/search/src/main/java/com/tangem/data/search/di/SearchDataModule.kt new file mode 100644 index 0000000000..e4a321d4da --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/di/SearchDataModule.kt @@ -0,0 +1,98 @@ +package com.tangem.data.search.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.data.search.model.SearchHistoryDTO +import com.tangem.data.search.repository.DefaultSearchRepository +import com.tangem.data.search.store.DefaultSearchHistoryStore +import com.tangem.data.search.store.SearchHistoryStore +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.search.repository.SearchRepository +import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase +import com.tangem.domain.search.usecase.GetSearchResultsUseCase +import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase +import com.tangem.domain.search.usecase.SaveSearchQueryUseCase +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object SearchDataModule { + + @OptIn(ExperimentalStdlibApi::class) + @Provides + @Singleton + fun provideSearchHistoryDataStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appScope: AppCoroutineScope, + ): DataStore { + return DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + defaultValue = SearchHistoryDTO(), + adapter = moshi.adapter(), + ), + produceFile = { context.dataStoreFile(fileName = "search_history") }, + scope = appScope, + ) + } + + @Provides + @Singleton + fun provideSearchHistoryStore(dataStore: DataStore): SearchHistoryStore { + return DefaultSearchHistoryStore(dataStore = dataStore) + } + + @Provides + @Singleton + fun provideSearchRepository( + store: SearchHistoryStore, + dispatchers: CoroutineDispatcherProvider, + ): SearchRepository { + return DefaultSearchRepository( + store = store, + dispatchers = dispatchers, + ) + } + + @Provides + fun provideGetSearchResultsUseCase( + searchRepository: SearchRepository, + multiAccountStatusListSupplier: MultiAccountStatusListSupplier, + userWalletsListRepository: UserWalletsListRepository, + ): GetSearchResultsUseCase { + return GetSearchResultsUseCase( + searchRepository = searchRepository, + multiAccountStatusListSupplier = multiAccountStatusListSupplier, + userWalletsListRepository = userWalletsListRepository, + ) + } + + @Provides + fun provideSaveSearchQueryUseCase(searchRepository: SearchRepository): SaveSearchQueryUseCase { + return SaveSearchQueryUseCase(searchRepository = searchRepository) + } + + @Provides + fun provideSaveRecentSearchTokenUseCase(searchRepository: SearchRepository): SaveRecentSearchTokenUseCase { + return SaveRecentSearchTokenUseCase(searchRepository = searchRepository) + } + + @Provides + fun provideClearSearchHistoryUseCase(searchRepository: SearchRepository): ClearSearchHistoryUseCase { + return ClearSearchHistoryUseCase(searchRepository = searchRepository) + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/model/SearchHistory.kt b/data/search/src/main/java/com/tangem/data/search/model/SearchHistory.kt new file mode 100644 index 0000000000..3166dd4d71 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/model/SearchHistory.kt @@ -0,0 +1,24 @@ +package com.tangem.data.search.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SearchHistoryDTO( + val textHints: List = emptyList(), + val recentTokens: List = emptyList(), +) + +@JsonClass(generateAdapter = true) +internal data class TextHintDTO( + val text: String, + val timestamp: Long, +) + +@JsonClass(generateAdapter = true) +internal data class RecentTokenDTO( + val id: String, + val name: String, + val symbol: String, + val imageUrl: String?, + val timestamp: Long, +) \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/repository/DefaultSearchRepository.kt b/data/search/src/main/java/com/tangem/data/search/repository/DefaultSearchRepository.kt new file mode 100644 index 0000000000..def344a019 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/repository/DefaultSearchRepository.kt @@ -0,0 +1,60 @@ +package com.tangem.data.search.repository + +import com.tangem.data.search.converter.RecentSearchTokenToRecentTokenDTOConverter +import com.tangem.data.search.converter.RecentTokenDTOToRecentSearchTokenConverter +import com.tangem.data.search.converter.TextHintDTOToSearchTextHintConverter +import com.tangem.data.search.model.TextHintDTO +import com.tangem.data.search.store.SearchHistoryStore +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.model.SearchTextHint +import com.tangem.domain.search.repository.SearchRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +internal class DefaultSearchRepository( + private val store: SearchHistoryStore, + private val dispatchers: CoroutineDispatcherProvider, +) : SearchRepository { + + private val textHintConverter by lazy { + TextHintDTOToSearchTextHintConverter() + } + private val recentTokenConverter by lazy { + RecentTokenDTOToRecentSearchTokenConverter() + } + private val recentTokenDMConverter by lazy { + RecentSearchTokenToRecentTokenDTOConverter() + } + + override fun getTextHints(): Flow> { + return store.getTextHints() + .map { textHintConverter.convertList(it) } + .flowOn(dispatchers.io) + } + + override fun getRecentTokens(): Flow> { + return store.getRecentTokens() + .map { recentTokenConverter.convertList(it) } + .flowOn(dispatchers.io) + } + + override suspend fun saveTextHint(text: String) = withContext(dispatchers.io) { + store.saveTextHint( + TextHintDTO( + text = text, + timestamp = System.currentTimeMillis(), + ), + ) + } + + override suspend fun saveRecentToken(token: RecentSearchToken) = withContext(dispatchers.io) { + store.saveRecentToken(recentTokenDMConverter.convert(token)) + } + + override suspend fun clearHistory() = withContext(dispatchers.io) { + store.clearAll() + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/store/DefaultSearchHistoryStore.kt b/data/search/src/main/java/com/tangem/data/search/store/DefaultSearchHistoryStore.kt new file mode 100644 index 0000000000..348328faa1 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/store/DefaultSearchHistoryStore.kt @@ -0,0 +1,53 @@ +package com.tangem.data.search.store + +import androidx.datastore.core.DataStore +import com.tangem.data.search.model.RecentTokenDTO +import com.tangem.data.search.model.SearchHistoryDTO +import com.tangem.data.search.model.TextHintDTO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class DefaultSearchHistoryStore( + private val dataStore: DataStore, +) : SearchHistoryStore { + + override fun getTextHints(): Flow> { + return dataStore.data.map { it.textHints.sortedByDescending(TextHintDTO::timestamp) } + } + + override fun getRecentTokens(): Flow> { + return dataStore.data.map { it.recentTokens.sortedByDescending(RecentTokenDTO::timestamp) } + } + + override suspend fun saveTextHint(hint: TextHintDTO) { + dataStore.updateData { current -> + val updated = current.textHints + .filter { it.text != hint.text } + .toMutableList() + .apply { add(0, hint) } + .take(MAX_HISTORY_SIZE) + current.copy(textHints = updated) + } + } + + override suspend fun saveRecentToken(token: RecentTokenDTO) { + dataStore.updateData { current -> + val updated = current.recentTokens + .filter { it.id != token.id } + .toMutableList() + .apply { add(0, token) } + .take(MAX_HISTORY_SIZE) + current.copy(recentTokens = updated) + } + } + + override suspend fun clearAll() { + dataStore.updateData { + SearchHistoryDTO() + } + } + + private companion object { + const val MAX_HISTORY_SIZE = 3 + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/store/SearchHistoryStore.kt b/data/search/src/main/java/com/tangem/data/search/store/SearchHistoryStore.kt new file mode 100644 index 0000000000..02b398d0e5 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/store/SearchHistoryStore.kt @@ -0,0 +1,13 @@ +package com.tangem.data.search.store + +import com.tangem.data.search.model.RecentTokenDTO +import com.tangem.data.search.model.TextHintDTO +import kotlinx.coroutines.flow.Flow + +internal interface SearchHistoryStore { + fun getTextHints(): Flow> + fun getRecentTokens(): Flow> + suspend fun saveTextHint(hint: TextHintDTO) + suspend fun saveRecentToken(token: RecentTokenDTO) + suspend fun clearAll() +} \ No newline at end of file diff --git a/domain/search/.gitignore b/domain/search/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/search/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/search/build.gradle.kts b/domain/search/build.gradle.kts new file mode 100644 index 0000000000..e13e688d24 --- /dev/null +++ b/domain/search/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.search" +} + +dependencies { + api(projects.domain.core) + api(projects.domain.models) + implementation(projects.domain.common) + implementation(projects.domain.markets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.appCurrency) + implementation(projects.domain.account) + implementation(projects.domain.account.status) +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/RecentSearchToken.kt b/domain/search/src/main/java/com/tangem/domain/search/model/RecentSearchToken.kt new file mode 100644 index 0000000000..d859f1d2a4 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/RecentSearchToken.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.search.model + +import com.tangem.domain.models.currency.CryptoCurrency + +/** + * @property timestamp epoch milliseconds + */ +data class RecentSearchToken( + val id: CryptoCurrency.RawID, + val name: String, + val symbol: String, + val imageUrl: String?, + val timestamp: Long, +) \ No newline at end of file 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 new file mode 100644 index 0000000000..784f334bdb --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.search.model + +import com.tangem.domain.markets.TokenMarket + +data class SearchResult( + val textHints: List, + val recentTokens: List, + val userAssets: List, + val marketTokens: List, +) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/SearchTextHint.kt b/domain/search/src/main/java/com/tangem/domain/search/model/SearchTextHint.kt new file mode 100644 index 0000000000..b949c8b3fa --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/SearchTextHint.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.search.model + +/** + * @property timestamp epoch milliseconds + */ +data class SearchTextHint(val text: String, val timestamp: Long) \ 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 new file mode 100644 index 0000000000..fd64a23e24 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt @@ -0,0 +1,14 @@ +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.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId + +data class UserAssetSearchEntry( + val userWalletId: UserWalletId, + val userWalletName: String, + val accountId: AccountId, + val accountName: AccountName, + val currencyStatus: CryptoCurrencyStatus, +) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/repository/SearchRepository.kt b/domain/search/src/main/java/com/tangem/domain/search/repository/SearchRepository.kt new file mode 100644 index 0000000000..f7cea5e075 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/repository/SearchRepository.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.search.repository + +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.model.SearchTextHint +import kotlinx.coroutines.flow.Flow + +/** + * Repository responsible for managing local search history storage. + * Handles persistence of user's past search queries and recently viewed market tokens. + * Each history type is limited to 3 entries, sorted by timestamp in descending order. + */ +interface SearchRepository { + + /** Observes the list of saved text hints, sorted by timestamp descending. */ + fun getTextHints(): Flow> + + /** Observes the list of recently viewed market tokens, sorted by timestamp descending. */ + fun getRecentTokens(): Flow> + + /** + * Saves a text hint to the search history. + * If the hint already exists, its timestamp is updated. Oldest entries are evicted when the limit is exceeded. + * + * @param text the search query text to save + */ + suspend fun saveTextHint(text: String) + + /** + * Saves a recently viewed market token to the search history. + * If a token with the same ID already exists, it is moved to the top. Oldest entries are evicted when the limit + * is exceeded. + * + * @param token the market token entry to save + */ + suspend fun saveRecentToken(token: RecentSearchToken) + + /** Clears all search history, including both text hints and recent tokens. */ + suspend fun clearHistory() +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/ClearSearchHistoryUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/ClearSearchHistoryUseCase.kt new file mode 100644 index 0000000000..f12c208257 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/ClearSearchHistoryUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.search.repository.SearchRepository + +/** + * Clears the entire search history, removing both text hints and recently viewed tokens. + * + * @property searchRepository local search history storage + */ +class ClearSearchHistoryUseCase( + private val searchRepository: SearchRepository, +) { + + suspend operator fun invoke() { + searchRepository.clearHistory() + } +} \ 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 new file mode 100644 index 0000000000..87e48a0215 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt @@ -0,0 +1,126 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +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.repository.SearchRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf + +/** + * Primary search use case that produces [SearchResult] based on the current query. + * + * Behavior depends on the query: + * - **Empty query** — returns search history: text hints and recently viewed tokens. + * - **Non-empty query** — performs the search across all unlocked user wallets, + * matching currencies by name or symbol, and combines the results with externally provided market tokens. + * + * @property searchRepository local search history storage + * @property multiAccountStatusListSupplier supplier for loaded account status lists across all wallets + * @property userWalletsListRepository repository providing the list of user wallets + */ +class GetSearchResultsUseCase( + private val searchRepository: SearchRepository, + private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, + private val userWalletsListRepository: UserWalletsListRepository, +) { + + /** + * Produces a [Flow] of [SearchResult] for the given [query]. + * + * @param query the search query string; blank means "show history" + * @param marketTokens external flow of market token search results (provided by presentation layer) + */ + operator fun invoke( + query: String, + marketTokens: Flow> = flowOf(emptyList()), + ): Flow { + return if (query.isBlank()) { + observeHistory() + } else { + searchAssets(query, marketTokens) + } + } + + private fun observeHistory(): Flow { + return combine( + searchRepository.getTextHints(), + searchRepository.getRecentTokens(), + ) { hints, tokens -> + SearchResult( + textHints = hints, + recentTokens = tokens, + userAssets = emptyList(), + marketTokens = emptyList(), + ) + } + } + + private fun searchAssets(query: String, marketTokens: Flow>): Flow { + return combine( + observeUserAssets(query), + marketTokens, + ) { userAssets, markets -> + SearchResult( + textHints = emptyList(), + recentTokens = emptyList(), + userAssets = userAssets, + marketTokens = markets, + ) + } + } + + private fun observeUserAssets(query: String): Flow> { + val lowerQuery = query.lowercase() + return combine( + multiAccountStatusListSupplier(), + userWalletsListRepository.userWallets, + ) { statusLists, wallets -> + val unlockedWallets = wallets + .orEmpty() + .filterNot(UserWallet::isLocked) + .associateBy { it.walletId } + + if (unlockedWallets.isEmpty()) return@combine emptyList() + + statusLists + .filter { it.userWalletId in unlockedWallets } + .flatMap { statusList -> extractMatchingAssets(statusList, unlockedWallets, lowerQuery) } + } + } + + private fun extractMatchingAssets( + statusList: AccountStatusList, + wallets: Map, + lowerQuery: String, + ): List { + val wallet = wallets[statusList.userWalletId] ?: return emptyList() + return statusList.accountStatuses + .filterCryptoPortfolio() + .flatMap { accountStatus -> + accountStatus.flattenCurrencies() + .filter { currencyStatus -> + val name = currencyStatus.currency.name.lowercase() + val symbol = currencyStatus.currency.symbol.lowercase() + name.contains(lowerQuery) || symbol.contains(lowerQuery) + } + .map { currencyStatus -> + UserAssetSearchEntry( + userWalletId = statusList.userWalletId, + userWalletName = wallet.name, + accountId = accountStatus.accountId, + accountName = accountStatus.account.accountName, + currencyStatus = currencyStatus, + ) + } + } + } +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveRecentSearchTokenUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveRecentSearchTokenUseCase.kt new file mode 100644 index 0000000000..9c8074fd7c --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveRecentSearchTokenUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.repository.SearchRepository + +/** + * Saves a market token to the "recently viewed" search history. + * Only market assets should be saved (user's own assets are ignored). + * The history is limited to 3 entries; oldest entries are evicted automatically. + * + * @property searchRepository local search history storage + */ +class SaveRecentSearchTokenUseCase( + private val searchRepository: SearchRepository, +) { + + /** + * @param token the market token to persist as a recent search entry + */ + suspend operator fun invoke(token: RecentSearchToken) { + searchRepository.saveRecentToken(token) + } +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveSearchQueryUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveSearchQueryUseCase.kt new file mode 100644 index 0000000000..486d9f40bd --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveSearchQueryUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.search.repository.SearchRepository + +/** + * Saves the current search query text to the local search history. + * Blank queries are ignored. The history is limited to 3 entries; oldest entries are evicted automatically. + * Should be called when the user selects any asset from the search results. + * + * @property searchRepository local search history storage + */ +class SaveSearchQueryUseCase( + private val searchRepository: SearchRepository, +) { + + /** + * @param query the search text to persist; blank values are silently ignored + */ + suspend operator fun invoke(query: String) { + if (query.isBlank()) return + searchRepository.saveTextHint(query.trim()) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4a3ebd13c0..01dadd7270 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -392,6 +392,7 @@ include(":domain:yield-supply") include(":domain:yield-supply:models") include(":domain:news") include(":domain:earn") +include(":domain:search") // endregion Domain modules // region Data modules @@ -430,4 +431,5 @@ include(":data:wallet-manager") include(":data:yield-supply") include(":data:news") include(":data:earn") +include(":data:search") // endregion Data modules \ No newline at end of file