Show recent searches from local input
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "770e86deaca12c1e4d82a71f6613da37",
|
||||
"identityHash": "5f7afc4947b08cff2f59cfb44bfbf281",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "BroadcastNostrEventReceipt",
|
||||
@@ -753,6 +753,41 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "RecentSearch",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, `synchronizationFilters` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`query`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "query",
|
||||
"columnName": "query",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "synchronizationFilters",
|
||||
"columnName": "synchronizationFilters",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "Repost",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `repostedPostId` TEXT NOT NULL, `repostedPostTags` TEXT NOT NULL, `repostedPostCreatedAt` INTEGER NOT NULL, `repostedPostContent` TEXT NOT NULL, `repostedPostAuthorPublicKey` TEXT NOT NULL, `content` TEXT NOT NULL, `nostrEventId` TEXT NOT NULL, `profilePublicKey` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`profilePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`repostedPostAuthorPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`repostedPostId`) REFERENCES `Post`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
@@ -1374,7 +1409,7 @@
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '770e86deaca12c1e4d82a71f6613da37')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5f7afc4947b08cff2f59cfb44bfbf281')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import ac.aux.compose.database.dao.NostrEventDao
|
||||
import ac.aux.compose.database.dao.PostDao
|
||||
import ac.aux.compose.database.dao.ProfileDao
|
||||
import ac.aux.compose.database.dao.ReactionDao
|
||||
import ac.aux.compose.database.dao.RecentSearchDao
|
||||
import ac.aux.compose.database.dao.RepostDao
|
||||
import ac.aux.compose.database.dao.SynchronizeNostrEventRequestDao
|
||||
import ac.aux.compose.database.dao.SynchronizeNostrEventResultDao
|
||||
@@ -19,6 +20,7 @@ import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.Post
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.Reaction
|
||||
import ac.aux.compose.database.model.RecentSearch
|
||||
import ac.aux.compose.database.model.Repost
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventResult
|
||||
@@ -41,6 +43,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
|
||||
Post::class,
|
||||
Profile::class,
|
||||
Reaction::class,
|
||||
RecentSearch::class,
|
||||
Repost::class,
|
||||
SynchronizeNostrEventRequest::class,
|
||||
SynchronizeNostrEventResult::class,
|
||||
@@ -59,6 +62,8 @@ abstract class AuxDatabase: RoomDatabase() {
|
||||
abstract fun postDao(): PostDao
|
||||
abstract fun profileDao(): ProfileDao
|
||||
abstract fun reactionDao(): ReactionDao
|
||||
|
||||
abstract fun recentSearchDao(): RecentSearchDao
|
||||
abstract fun repostDao(): RepostDao
|
||||
|
||||
abstract fun synchronizeNostrEventRequestDao(): SynchronizeNostrEventRequestDao
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package ac.aux.compose.database.dao
|
||||
|
||||
import ac.aux.compose.database.model.RecentSearch
|
||||
import ac.aux.compose.database.model.Zap
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface RecentSearchDao {
|
||||
@Query("SELECT * FROM RecentSearch WHERE `query` = :query")
|
||||
fun observeRecentSearchByQuery(query: String): Flow<RecentSearch?>
|
||||
|
||||
@Query("SELECT * FROM RecentSearch ORDER BY createdAt DESC LIMIT :limit ")
|
||||
fun getAllRecentSearches(limit: Int = 21): List<RecentSearch>
|
||||
|
||||
@Delete
|
||||
fun deleteRecentSearch(recentSearch: RecentSearch)
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(recentSearch: RecentSearch)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package ac.aux.compose.database.model
|
||||
|
||||
import ac.aux.compose.database.model.traits.TimestampedEntity
|
||||
import ac.aux.compose.database.model.typealiases.SynchronizationFilterArray
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Entity
|
||||
data class RecentSearch(
|
||||
@PrimaryKey
|
||||
val query: String,
|
||||
|
||||
val synchronizationFilters: SynchronizationFilterArray? = null,
|
||||
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val updatedAt: Instant = createdAt,
|
||||
): TimestampedEntity {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as RecentSearch
|
||||
|
||||
if (query != other.query) return false
|
||||
if (!synchronizationFilters.contentEquals(other.synchronizationFilters)) return false
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (updatedAt != other.updatedAt) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = query.hashCode()
|
||||
result = 31 * result + (synchronizationFilters?.contentHashCode() ?: 0)
|
||||
result = 31 * result + createdAt.hashCode()
|
||||
result = 31 * result + updatedAt.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,13 @@ import ac.aux.compose.database.model.BroadcastNostrEventReceipt
|
||||
import ac.aux.compose.database.model.BroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.RecentSearch
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import ac.aux.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.intermdiate.LocalNostrEvent
|
||||
import ac.aux.compose.database.model.intermdiate.LocalProfile
|
||||
import ac.aux.compose.database.model.typealiases.SynchronizationFilterArray
|
||||
import ac.aux.compose.nostr.Relays
|
||||
import ac.aux.compose.repository.NostrRepository
|
||||
import co.touchlab.kermit.Logger
|
||||
@@ -324,8 +326,23 @@ class DatabaseNostrRepository(
|
||||
return database.profileDao().getAllProfiles()
|
||||
}
|
||||
|
||||
override suspend fun recentSearches(): List<String> {
|
||||
TODO("Not yet implemented")
|
||||
override suspend fun getRecentSearches(): List<RecentSearch> {
|
||||
return database.recentSearchDao().getAllRecentSearches()
|
||||
}
|
||||
|
||||
override suspend fun removeRecentSearch(recentSearch: RecentSearch) {
|
||||
database.recentSearchDao().deleteRecentSearch(recentSearch)
|
||||
}
|
||||
|
||||
override suspend fun saveSearchQuery(
|
||||
query: String,
|
||||
synchronizationFilterArray: SynchronizationFilterArray?
|
||||
) {
|
||||
database.recentSearchDao().upsert(
|
||||
RecentSearch(
|
||||
query = query,
|
||||
synchronizationFilters = synchronizationFilterArray
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import ac.aux.compose.database.model.BroadcastNostrEventReceipt
|
||||
import ac.aux.compose.database.model.BroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.RecentSearch
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import ac.aux.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.intermdiate.LocalNostrEvent
|
||||
import ac.aux.compose.database.model.intermdiate.LocalProfile
|
||||
import ac.aux.compose.database.model.typealiases.SynchronizationFilterArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -73,8 +75,11 @@ interface NostrRepository {
|
||||
|
||||
suspend fun searchableProfiles(): List<Profile>
|
||||
|
||||
suspend fun recentSearches(): List<String>
|
||||
suspend fun getRecentSearches(): List<RecentSearch>
|
||||
|
||||
suspend fun removeRecentSearch(recentSearch: RecentSearch)
|
||||
|
||||
suspend fun saveSearchQuery(query: String, synchronizationFilterArray: SynchronizationFilterArray? = null)
|
||||
|
||||
companion object {
|
||||
val NO_OP_NOSTR_REPOSITORY = object : NostrRepository {
|
||||
@@ -168,7 +173,18 @@ interface NostrRepository {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun recentSearches(): List<String> {
|
||||
override suspend fun getRecentSearches(): List<RecentSearch> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun removeRecentSearch(recentSearch: RecentSearch) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun saveSearchQuery(
|
||||
query: String,
|
||||
synchronizationFilterArray: SynchronizationFilterArray?
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,9 +127,6 @@ fun FeedScreen(
|
||||
is FeedListUIState.Loading -> {
|
||||
LoadingDataIndicator()
|
||||
|
||||
LaunchedEffect(true) {
|
||||
feedListViewModel.loadNostrFeed()
|
||||
}
|
||||
}
|
||||
FeedListUIState.Error -> {
|
||||
Text("Something went wrong")
|
||||
|
||||
@@ -125,66 +125,6 @@ fun SearchScreen(
|
||||
"Zinfandel",
|
||||
)
|
||||
|
||||
val recentSearches = remember {
|
||||
mutableStateListOf(
|
||||
"Alligator",
|
||||
"Baboon",
|
||||
"Camel",
|
||||
"Deer",
|
||||
"Earthworm",
|
||||
"Falcon",
|
||||
"Gazelle",
|
||||
"Hamster",
|
||||
"Iguana",
|
||||
"Jellyfish",
|
||||
"Koala",
|
||||
"Leopard",
|
||||
"Meerkat",
|
||||
"Nilgai",
|
||||
"Octopus",
|
||||
"Panda",
|
||||
"Quokka",
|
||||
"Panda",
|
||||
"Rabbit",
|
||||
"Scorpion",
|
||||
"Tarantula",
|
||||
"Uakari",
|
||||
"Viper",
|
||||
"Walrus",
|
||||
"Xenops",
|
||||
"Yak",
|
||||
"Zebra",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val profileSearches = listOf(
|
||||
"Afghanistan",
|
||||
"Bahamas",
|
||||
"Cambodia",
|
||||
"Denmark",
|
||||
"Egypt",
|
||||
"Finland",
|
||||
"Gabon",
|
||||
"Haiti",
|
||||
"Iran",
|
||||
"Jamaica",
|
||||
"Kazakhstan",
|
||||
"Lesotho",
|
||||
"Malawi",
|
||||
"Namibia",
|
||||
"Oman",
|
||||
"Pakistan",
|
||||
"Qatar",
|
||||
"Russia",
|
||||
"Samoa",
|
||||
"Senegal",
|
||||
"Tanzania",
|
||||
"Uganda",
|
||||
"Venezuela",
|
||||
"Yemen",
|
||||
"Zambia"
|
||||
)
|
||||
val textFieldState = rememberTextFieldState()
|
||||
// Controls expansion state of the search bar
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -363,10 +303,6 @@ fun SearchScreen(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
|
||||
|
||||
|
||||
LazyColumn {
|
||||
// Display search results in a scrollable column
|
||||
item {
|
||||
@@ -430,21 +366,26 @@ fun SearchScreen(
|
||||
}
|
||||
}
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Recents",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (searchViewModel.recentSearches.isNotEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Recents",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
itemsIndexed(recentSearches) { index, item ->
|
||||
items(
|
||||
items = searchViewModel.recentSearches,
|
||||
key = { recentSearch -> recentSearch.query }
|
||||
) { recentSearch ->
|
||||
ListItem(
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
headlineContent = { Text(item) },
|
||||
headlineContent = { Text(recentSearch.query) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Default.YoutubeSearchedFor,
|
||||
@@ -455,7 +396,7 @@ fun SearchScreen(
|
||||
.clickable {
|
||||
onNavigateToSearchResult.invoke(
|
||||
SearchResultRoute(
|
||||
query = item
|
||||
query = recentSearch.query
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -463,7 +404,7 @@ fun SearchScreen(
|
||||
trailingContent = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
recentSearches.removeAt(index)
|
||||
searchViewModel.removeRecentSearch(recentSearch)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
|
||||
@@ -37,20 +37,14 @@ class FeedListViewModel(
|
||||
val isSynchronizationPending: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
init {
|
||||
observeSynchronizeNostrEventRequestByPurposeAndStatusCount()
|
||||
observeLocalNostrFeed()
|
||||
}
|
||||
|
||||
fun retryLoad() {
|
||||
feedListUIState = FeedListUIState.Loading
|
||||
loadNostrFeed()
|
||||
}
|
||||
|
||||
fun loadNostrFeed() {
|
||||
logger.d("init")
|
||||
scheduleSynchronization()
|
||||
observeLocalNostrFeed()
|
||||
observeSynchronizeNostrEventRequestByPurposeAndStatusCount()
|
||||
}
|
||||
|
||||
fun observeLocalNostrFeed() {
|
||||
logger.d("observeLocalNostrFeed")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// Get contact list and sync feed...
|
||||
|
||||
@@ -70,6 +64,7 @@ class FeedListViewModel(
|
||||
}
|
||||
|
||||
fun scheduleSynchronization() {
|
||||
logger.d("scheduleSynchronization")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// Sync Notifications... might want to also run this in the background
|
||||
nostrRepository.queueSynchronizeNostrEvent(
|
||||
@@ -98,6 +93,7 @@ class FeedListViewModel(
|
||||
}
|
||||
|
||||
fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount() {
|
||||
logger.d("observeSynchronizeNostrEventRequestByPurposeAndStatusCount")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
nostrRepository.observeSynchronizeNostrEventRequestByPurposeAndStatusCount("feed").collect { count ->
|
||||
logger.d("Pending/Processing Synchronization Request: $count")
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package ac.aux.compose.ui.view.model
|
||||
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.RecentSearch
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.database.model.intermdiate.LocalProfile
|
||||
import ac.aux.compose.database.model.types.SynchronizationFilter
|
||||
import ac.aux.compose.managers.SeedManager
|
||||
import ac.aux.compose.nostr.Relays
|
||||
import ac.aux.compose.repository.NostrRepository
|
||||
import ac.aux.compose.repository.SearchRepository
|
||||
import ac.aux.compose.ui.view.state.SearchUIState
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -20,7 +18,6 @@ import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
@@ -42,32 +39,21 @@ class SearchViewModel(
|
||||
var searchableProfiles = mutableStateListOf<Profile>()
|
||||
private set
|
||||
|
||||
var recentSearches = mutableStateListOf<RecentSearch>()
|
||||
private set
|
||||
|
||||
init {
|
||||
loadSearchableProfiles()
|
||||
loadRecentSearches()
|
||||
}
|
||||
|
||||
fun performSearch(searchQuery: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// Sync Notifications... might want to also run this in the background
|
||||
searchRepository.submitSearch(
|
||||
Relays.eventPublishRelaySet.map { normalizedRelay ->
|
||||
SynchronizeNostrEventRequest(
|
||||
purpose = "search",
|
||||
synchronizationFilters = arrayOf(
|
||||
SynchronizationFilter(
|
||||
kinds = arrayOf(
|
||||
TextNoteEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
),
|
||||
search = "shoes",
|
||||
limit = 50
|
||||
)
|
||||
),
|
||||
relayURL = normalizedRelay.url
|
||||
)
|
||||
}
|
||||
nostrRepository.saveSearchQuery(
|
||||
searchQuery,
|
||||
// TODO: synchronizationFilterArray
|
||||
)
|
||||
// TODO: Run search on local db for suggestions...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +65,21 @@ class SearchViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun loadRecentSearches() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
recentSearches.addAll(
|
||||
nostrRepository.getRecentSearches()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRecentSearch(recentSearch: RecentSearch) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
nostrRepository.removeRecentSearch(recentSearch)
|
||||
recentSearches.remove(recentSearch)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "SearchViewModel"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user