Skip to content

Commit e52e753

Browse files
committed
feat: implement search history functionality
- Add `SearchHistoryEntity` and `SearchHistoryDao` to persist recent search queries in the Room database (Schema version 7). - Implement `SearchHistoryRepository` to manage adding, removing, and clearing search history entries. - Introduce `SearchHistorySection` component to display recent searches when the search query is empty. - Update `SearchViewModel` to observe search history and save valid queries (min 3 characters). - Remove the debounced automatic search logic, favoring manual search triggers and history interactions. - Add localized string resources for "Recent searches", "Clear all", and "Remove" actions. - Register new search history components in the dependency injection modules.
1 parent dfd28d0 commit e52e753

13 files changed

Lines changed: 881 additions & 39 deletions

File tree

core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/7.json

Lines changed: 586 additions & 0 deletions
Large diffs are not rendered by default.

core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import zed.rainxch.core.data.local.db.AppDatabase
1515
import zed.rainxch.core.data.local.db.dao.CacheDao
1616
import zed.rainxch.core.data.local.db.dao.FavoriteRepoDao
1717
import zed.rainxch.core.data.local.db.dao.InstalledAppDao
18+
import zed.rainxch.core.data.local.db.dao.SearchHistoryDao
1819
import zed.rainxch.core.data.local.db.dao.SeenRepoDao
1920
import zed.rainxch.core.data.local.db.dao.StarredRepoDao
2021
import zed.rainxch.core.data.local.db.dao.UpdateHistoryDao
@@ -27,6 +28,7 @@ import zed.rainxch.core.data.repository.FavouritesRepositoryImpl
2728
import zed.rainxch.core.data.repository.InstalledAppsRepositoryImpl
2829
import zed.rainxch.core.data.repository.ProxyRepositoryImpl
2930
import zed.rainxch.core.data.repository.RateLimitRepositoryImpl
31+
import zed.rainxch.core.data.repository.SearchHistoryRepositoryImpl
3032
import zed.rainxch.core.data.repository.SeenReposRepositoryImpl
3133
import zed.rainxch.core.data.repository.StarredRepositoryImpl
3234
import zed.rainxch.core.data.repository.TweaksRepositoryImpl
@@ -39,6 +41,7 @@ import zed.rainxch.core.domain.repository.FavouritesRepository
3941
import zed.rainxch.core.domain.repository.InstalledAppsRepository
4042
import zed.rainxch.core.domain.repository.ProxyRepository
4143
import zed.rainxch.core.domain.repository.RateLimitRepository
44+
import zed.rainxch.core.domain.repository.SearchHistoryRepository
4245
import zed.rainxch.core.domain.repository.SeenReposRepository
4346
import zed.rainxch.core.domain.repository.StarredRepository
4447
import zed.rainxch.core.domain.repository.TweaksRepository
@@ -103,6 +106,12 @@ val coreModule =
103106
)
104107
}
105108

109+
single<SearchHistoryRepository> {
110+
SearchHistoryRepositoryImpl(
111+
searchHistoryDao = get(),
112+
)
113+
}
114+
106115
single<ProxyRepository> {
107116
ProxyRepositoryImpl(
108117
preferences = get(),
@@ -216,4 +225,8 @@ val databaseModule =
216225
single<SeenRepoDao> {
217226
get<AppDatabase>().seenRepoDao
218227
}
228+
229+
single<SearchHistoryDao> {
230+
get<AppDatabase>().searchHistoryDao
231+
}
219232
}

core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/AppDatabase.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import androidx.room.RoomDatabase
55
import zed.rainxch.core.data.local.db.dao.CacheDao
66
import zed.rainxch.core.data.local.db.dao.FavoriteRepoDao
77
import zed.rainxch.core.data.local.db.dao.InstalledAppDao
8+
import zed.rainxch.core.data.local.db.dao.SearchHistoryDao
89
import zed.rainxch.core.data.local.db.dao.SeenRepoDao
910
import zed.rainxch.core.data.local.db.dao.StarredRepoDao
1011
import zed.rainxch.core.data.local.db.dao.UpdateHistoryDao
1112
import zed.rainxch.core.data.local.db.entities.CacheEntryEntity
1213
import zed.rainxch.core.data.local.db.entities.FavoriteRepoEntity
1314
import zed.rainxch.core.data.local.db.entities.InstalledAppEntity
15+
import zed.rainxch.core.data.local.db.entities.SearchHistoryEntity
1416
import zed.rainxch.core.data.local.db.entities.SeenRepoEntity
1517
import zed.rainxch.core.data.local.db.entities.StarredRepositoryEntity
1618
import zed.rainxch.core.data.local.db.entities.UpdateHistoryEntity
@@ -23,8 +25,9 @@ import zed.rainxch.core.data.local.db.entities.UpdateHistoryEntity
2325
StarredRepositoryEntity::class,
2426
CacheEntryEntity::class,
2527
SeenRepoEntity::class,
28+
SearchHistoryEntity::class,
2629
],
27-
version = 6,
30+
version = 7,
2831
exportSchema = true,
2932
)
3033
abstract class AppDatabase : RoomDatabase() {
@@ -34,4 +37,5 @@ abstract class AppDatabase : RoomDatabase() {
3437
abstract val starredReposDao: StarredRepoDao
3538
abstract val cacheDao: CacheDao
3639
abstract val seenRepoDao: SeenRepoDao
40+
abstract val searchHistoryDao: SearchHistoryDao
3741
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package zed.rainxch.core.data.local.db.dao
2+
3+
import androidx.room.Dao
4+
import androidx.room.Insert
5+
import androidx.room.OnConflictStrategy
6+
import androidx.room.Query
7+
import kotlinx.coroutines.flow.Flow
8+
import zed.rainxch.core.data.local.db.entities.SearchHistoryEntity
9+
10+
@Dao
11+
interface SearchHistoryDao {
12+
@Query("SELECT * FROM search_history ORDER BY searchedAt DESC LIMIT 20")
13+
fun getRecentSearches(): Flow<List<SearchHistoryEntity>>
14+
15+
@Insert(onConflict = OnConflictStrategy.REPLACE)
16+
suspend fun insert(entity: SearchHistoryEntity)
17+
18+
@Query("DELETE FROM search_history WHERE `query` = :query")
19+
suspend fun deleteByQuery(query: String)
20+
21+
@Query("DELETE FROM search_history")
22+
suspend fun clearAll()
23+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package zed.rainxch.core.data.local.db.entities
2+
3+
import androidx.room.Entity
4+
import androidx.room.PrimaryKey
5+
6+
@Entity(tableName = "search_history")
7+
data class SearchHistoryEntity(
8+
@PrimaryKey
9+
val query: String,
10+
val searchedAt: Long,
11+
)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package zed.rainxch.core.data.repository
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.map
5+
import kotlin.time.Clock
6+
import zed.rainxch.core.data.local.db.dao.SearchHistoryDao
7+
import zed.rainxch.core.data.local.db.entities.SearchHistoryEntity
8+
import zed.rainxch.core.domain.repository.SearchHistoryRepository
9+
10+
class SearchHistoryRepositoryImpl(
11+
private val searchHistoryDao: SearchHistoryDao,
12+
) : SearchHistoryRepository {
13+
override fun getRecentSearches(): Flow<List<String>> =
14+
searchHistoryDao.getRecentSearches().map { entities ->
15+
entities.map { it.query }
16+
}
17+
18+
override suspend fun addSearch(query: String) {
19+
val trimmed = query.trim()
20+
if (trimmed.isBlank()) return
21+
searchHistoryDao.insert(
22+
SearchHistoryEntity(
23+
query = trimmed,
24+
searchedAt = Clock.System.now().toEpochMilliseconds(),
25+
),
26+
)
27+
}
28+
29+
override suspend fun removeSearch(query: String) {
30+
searchHistoryDao.deleteByQuery(query)
31+
}
32+
33+
override suspend fun clearAll() {
34+
searchHistoryDao.clearAll()
35+
}
36+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package zed.rainxch.core.domain.repository
2+
3+
import kotlinx.coroutines.flow.Flow
4+
5+
interface SearchHistoryRepository {
6+
fun getRecentSearches(): Flow<List<String>>
7+
8+
suspend fun addSearch(query: String)
9+
10+
suspend fun removeSearch(query: String)
11+
12+
suspend fun clearAll()
13+
}

core/presentation/src/commonMain/composeResources/values/strings.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,4 +615,9 @@
615615
<string name="clear_seen_history_description">Reset all seen repositories so they appear again in feeds</string>
616616
<string name="seen_history_cleared">Seen history cleared</string>
617617
<string name="seen_badge">Viewed</string>
618+
619+
<!-- Search history -->
620+
<string name="recent_searches">Recent searches</string>
621+
<string name="clear_all_history">Clear all</string>
622+
<string name="remove_search_history_item">Remove</string>
618623
</resources>

feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,14 @@ sealed interface SearchAction {
6161
data object OnFabClick : SearchAction
6262

6363
data object DismissClipboardBanner : SearchAction
64+
65+
data class OnHistoryItemClick(
66+
val query: String,
67+
) : SearchAction
68+
69+
data class OnRemoveHistoryItem(
70+
val query: String,
71+
) : SearchAction
72+
73+
data object OnClearAllHistory : SearchAction
6474
}

feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import zed.rainxch.core.presentation.theme.GithubStoreTheme
9292
import zed.rainxch.core.presentation.utils.ObserveAsEvents
9393
import zed.rainxch.githubstore.core.presentation.res.*
9494
import zed.rainxch.search.presentation.components.LanguageFilterBottomSheet
95+
import zed.rainxch.search.presentation.components.SearchHistorySection
9596
import zed.rainxch.search.presentation.components.SortByBottomSheet
9697
import zed.rainxch.search.presentation.model.ParsedGithubLink
9798
import zed.rainxch.search.presentation.model.ProgrammingLanguageUi
@@ -485,6 +486,26 @@ fun SearchScreen(
485486
)
486487
}
487488

489+
// Show search history when query is empty
490+
if (state.query.isBlank() &&
491+
state.repositories.isEmpty() &&
492+
state.recentSearches.isNotEmpty() &&
493+
!state.isLoading
494+
) {
495+
SearchHistorySection(
496+
recentSearches = state.recentSearches,
497+
onHistoryItemClick = { query ->
498+
onAction(SearchAction.OnHistoryItemClick(query))
499+
},
500+
onRemoveItem = { query ->
501+
onAction(SearchAction.OnRemoveHistoryItem(query))
502+
},
503+
onClearAll = {
504+
onAction(SearchAction.OnClearAllHistory)
505+
},
506+
)
507+
}
508+
488509
val visibleRepos by remember(state.repositories, state.isHideSeenEnabled, state.seenRepoIds) {
489510
derivedStateOf {
490511
if (state.isHideSeenEnabled && state.seenRepoIds.isNotEmpty()) {

0 commit comments

Comments
 (0)