diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt index 26cd9db0..da53e52c 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt @@ -211,6 +211,11 @@ abstract class NostrDao( synchronizeNostrEventRequests ) } + + nostrEvent.toProfile()?.let { profile -> + database.profileDao().upsert(profile) + } + nostrEvent.toPost()?.let { post -> if (post.replyToId != null) { // Find or create placeHolder note that is replyTo @@ -232,11 +237,20 @@ abstract class NostrDao( database.postDao().upsert(post) } - nostrEvent.toProfile()?.let { profile -> - database.profileDao().upsert(profile) - } - nostrEvent.toReaction()?.let { reaction -> + val replyingToPost = database.postDao().getPostById(reaction.replyToId) + + if (replyingToPost == null) { + // Persist PlaceHolder and sync + database.postDao().insert( + Post( + id = reaction.replyToId, + nostrEventId = nostrEvent.id, // Will get overwriting by sync handled by tagged events... + content = reaction.replyToId, + profilePublicKey = nostrEvent.pubKey // Will get overwriting by sync handled by tagged events... + ) + ) + } database.reactionDao().upsert(reaction) } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt index 795443b2..be023a80 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt @@ -1,6 +1,8 @@ package ac.aux.compose.database.dao import ac.aux.compose.database.model.Profile +import ac.aux.compose.database.model.intermdiate.LocalAccount +import ac.aux.compose.database.model.intermdiate.LocalProfile import androidx.room.Dao import androidx.room.Insert import androidx.room.Query @@ -11,6 +13,9 @@ interface ProfileDao { @Query("SELECT * FROM Profile LIMIT :limit") fun getAllProfiles(limit: Int = 21): List + @Query("SELECT * FROM NostrEvent WHERE kind = 0 AND content LIKE '%' || :searchTerm || '%' LIMIT :limit") + fun filterProfiles(searchTerm: String, limit: Int = 21): List + @Query("SELECT * FROM Profile WHERE publicKey = :publicKey") fun getProfileByPublicKey(publicKey: String): Profile? diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/UnsignedNostrEventDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/UnsignedNostrEventDao.kt index 646febb6..87e417f6 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/UnsignedNostrEventDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/UnsignedNostrEventDao.kt @@ -1,10 +1,8 @@ package ac.aux.compose.database.dao -import ac.aux.compose.database.model.NostrEvent import ac.aux.compose.database.model.UnsignedNostrEvent -import ac.aux.compose.database.model.intermdiate.LocalProfile +import ac.aux.compose.database.model.intermdiate.LocalAccount import androidx.room.Dao -import androidx.room.Insert import androidx.room.Query import androidx.room.Upsert import kotlinx.coroutines.flow.Flow @@ -24,7 +22,7 @@ interface UnsignedNostrEventDao { suspend fun upsert(unsignedNostrEvent: UnsignedNostrEvent): Long @Query("SELECT * FROM UnsignedNostrEvent WHERE kind = 0 AND pubKey = :publicKey") - fun observeProfile(publicKey: String): Flow + fun observeProfile(publicKey: String): Flow @Query("SELECT * FROM UnsignedNostrEvent WHERE pubKey = :publicKey AND signedAt IS NULL") // TODO: where nostrEvent.unsignedNostrEventId is 0 fun observeUnsignedNostrEvents(publicKey: String): Flow diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Profile.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Profile.kt index 323908bd..6633a6f6 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Profile.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Profile.kt @@ -52,4 +52,8 @@ data class Profile( override val viewedAt: Instant? = null, override val deletedAt: Instant? = null, override val broadcastedAt: Instant? = null, -): NostrEventEntity, TimestampedEntity, LocalStoreEntity, UserViewableEntity, BroadcastableEntity, SoftDeletableEntity \ No newline at end of file +): NostrEventEntity, TimestampedEntity, LocalStoreEntity, UserViewableEntity, BroadcastableEntity, SoftDeletableEntity { + fun humanReadableNameOrPubkey(): String { + return displayName ?: userName ?: nip05 ?: twitter ?: publicKey + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalAccount.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalAccount.kt new file mode 100644 index 00000000..4a7cfd82 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalAccount.kt @@ -0,0 +1,45 @@ +package ac.aux.compose.database.model.intermdiate + +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.SynchronizeNostrEventRequest +import ac.aux.compose.database.model.UnsignedNostrEvent +import androidx.room.Embedded +import androidx.room.Relation + +data class LocalAccount( + @Embedded val unsignedNostrEvent: UnsignedNostrEvent?, + + @Relation( + parentColumn = "id", + entityColumn = "unsignedNostrEventId" + ) + val nostrEvent: NostrEvent?, + + @Relation( + parentColumn = "pubKey", + entityColumn = "publicKey" + ) + val profile: Profile?, + + @Relation( + parentColumn = "id", + entityColumn = "unsignedNostrEventId" + ) + val broadcastNostrEventRequest: BroadcastNostrEventRequest?, + + @Relation( + parentColumn = "id", + entityColumn = "unsignedNostrEventId" + ) + val broadcastNostrEventReceipt: BroadcastNostrEventReceipt?, + + @Relation( + parentColumn = "id", + entityColumn = "unsignedNostrEventId" + ) + val synchronizeNostrEventRequests: List, + +) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalProfile.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalProfile.kt index 2685a116..a0905bf4 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalProfile.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalProfile.kt @@ -10,36 +10,11 @@ import androidx.room.Embedded import androidx.room.Relation data class LocalProfile( - @Embedded val unsignedNostrEvent: UnsignedNostrEvent?, - - @Relation( - parentColumn = "id", - entityColumn = "unsignedNostrEventId" - ) - val nostrEvent: NostrEvent?, + @Embedded val nostrEvent: NostrEvent, @Relation( parentColumn = "pubKey", entityColumn = "publicKey" ) - val profile: Profile?, - - @Relation( - parentColumn = "id", - entityColumn = "unsignedNostrEventId" - ) - val broadcastNostrEventRequest: BroadcastNostrEventRequest?, - - @Relation( - parentColumn = "id", - entityColumn = "unsignedNostrEventId" - ) - val broadcastNostrEventReceipt: BroadcastNostrEventReceipt?, - - @Relation( - parentColumn = "id", - entityColumn = "unsignedNostrEventId" - ) - val synchronizeNostrEventRequests: List, - + val profile: Profile, ) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt index b6dc826f..1b526bba 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt @@ -11,6 +11,7 @@ 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.LocalAccount import ac.aux.compose.database.model.intermdiate.LocalProfile import ac.aux.compose.database.model.typealiases.SynchronizationFilterArray import ac.aux.compose.nostr.Relays @@ -49,7 +50,7 @@ class DatabaseNostrRepository( val logger = Logger.withTag(TAG) - override suspend fun observeProfile(publicKey: HexKey): Flow { + override suspend fun observeProfile(publicKey: HexKey): Flow { return database.unsignedNostrEventDao().observeProfile(publicKey) } @@ -326,6 +327,10 @@ class DatabaseNostrRepository( return database.profileDao().getAllProfiles() } + override suspend fun filterProfiles(searchTerm: String): List { + return database.profileDao().filterProfiles(searchTerm = searchTerm) + } + override suspend fun getRecentSearches(): List { return database.recentSearchDao().getAllRecentSearches() } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt index 317a6651..f706608c 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt @@ -9,13 +9,14 @@ 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.LocalAccount 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 interface NostrRepository { - suspend fun observeProfile(publicKey: HexKey): Flow + suspend fun observeProfile(publicKey: HexKey): Flow suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow @@ -75,6 +76,8 @@ interface NostrRepository { suspend fun searchableProfiles(): List + suspend fun filterProfiles(searchTerm: String): List + suspend fun getRecentSearches(): List suspend fun removeRecentSearch(recentSearch: RecentSearch) @@ -83,7 +86,7 @@ interface NostrRepository { companion object { val NO_OP_NOSTR_REPOSITORY = object : NostrRepository { - override suspend fun observeProfile(publicKey: HexKey): Flow { + override suspend fun observeProfile(publicKey: HexKey): Flow { TODO("Not yet implemented") } @@ -173,6 +176,10 @@ interface NostrRepository { TODO("Not yet implemented") } + override suspend fun filterProfiles(searchTerm: String): List { + TODO("Not yet implemented") + } + override suspend fun getRecentSearches(): List { TODO("Not yet implemented") } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/SearchRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/SearchRepository.kt index e6f04029..db371af3 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/SearchRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/SearchRepository.kt @@ -1,8 +1,6 @@ package ac.aux.compose.repository -import ac.aux.compose.database.model.Profile import ac.aux.compose.database.model.SynchronizeNostrEventRequest -import ac.aux.compose.database.model.intermdiate.LocalProfile interface SearchRepository { diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt index 35d866ea..ce28db5b 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt @@ -126,7 +126,6 @@ fun FeedScreen( when(val feedListUIState = feedListViewModel.feedListUIState) { is FeedListUIState.Loading -> { LoadingDataIndicator() - } FeedListUIState.Error -> { Text("Something went wrong") @@ -198,7 +197,7 @@ fun FeedScreen( if (feedListViewModel.isSynchronizationPending.value) { item { Spacer( - modifier = Modifier.height(50.dp) + modifier = Modifier.height(30.dp) ) LoadingIndicator() } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchScreen.kt index 2cd13d2c..2da283ab 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchScreen.kt @@ -97,34 +97,6 @@ fun SearchScreen( searchRepository = searchRepository ), ) - - val searchResults = listOf( - "Apple", - "Banna", - "Carrot", - "Durian", - "Eggplant", - "Fig", - "Grape", - "Honeydew", - "Jackfruit", - "Kiwi", - "Lemon", - "Mango", - "Nectarine", - "Orange", - "Papaya", - "Quince", - "Raspberry", - "Tangerine", - "Ugli Fruit", - "Vanilla Bean", - "Watermelon", - "Xigua", - "Yellow Passion Fruit", - "Zinfandel", - ) - val textFieldState = rememberTextFieldState() // Controls expansion state of the search bar var expanded by rememberSaveable { mutableStateOf(false) } @@ -149,7 +121,10 @@ fun SearchScreen( inputField = { SearchBarDefaults.InputField( query = textFieldState.text.toString(), - onQueryChange = { textFieldState.edit { replace(0, length, it) } }, + onQueryChange = { + textFieldState.edit { replace(0, length, it) } + searchViewModel.filterLocalProfiles(it) + }, onSearch = { if (textFieldState.text.isBlank()) { expanded = false @@ -265,9 +240,9 @@ fun SearchScreen( ) } - itemsIndexed(searchResults) { index, item -> + itemsIndexed(searchViewModel.filteredProfiles) { index, profile -> ListItem( - headlineContent = { Text(item) }, + headlineContent = { Text(profile.humanReadableNameOrPubkey()) }, supportingContent = { Text("Profile") }, @@ -275,7 +250,7 @@ fun SearchScreen( IconButton( onClick = { onNavigateToProfile.invoke( - ImplementationPendingRoute("Profile result") + NostrEventDetailRoute(profile.nostrEventId) ) } ) { @@ -287,10 +262,8 @@ fun SearchScreen( }, modifier = Modifier .clickable { - onNavigateToSearchResult.invoke( - SearchResultRoute( - item - ) + onNavigateToProfile.invoke( + NostrEventDetailRoute(profile.nostrEventId) ) } .fillMaxWidth(), diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/SearchViewModel.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/SearchViewModel.kt index 9ae4a90d..841905fb 100755 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/SearchViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/SearchViewModel.kt @@ -2,9 +2,6 @@ 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.types.SynchronizationFilter -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 @@ -18,10 +15,6 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import co.touchlab.kermit.Logger -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.launch @@ -39,6 +32,9 @@ class SearchViewModel( var searchableProfiles = mutableStateListOf() private set + var filteredProfiles = mutableStateListOf() + private set + var recentSearches = mutableStateListOf() private set @@ -47,6 +43,17 @@ class SearchViewModel( loadRecentSearches() } + fun filterLocalProfiles(searchTerm: String) { + viewModelScope.launch(Dispatchers.IO) { + val filteredLocalProfiles = nostrRepository.filterProfiles(searchTerm = searchTerm) + + filteredProfiles.clear() + filteredProfiles.addAll( + filteredLocalProfiles.map { it.profile } + ) + } + } + fun loadSearchableProfiles() { viewModelScope.launch(Dispatchers.IO) { searchableProfiles.addAll(