Show filtered list of locally stored profiles when search is being carried out.

This commit is contained in:
Kgothatso Ngako
2026-04-02 00:56:11 +02:00
parent 6ea09b8a7a
commit 8673d0b69f
12 changed files with 116 additions and 86 deletions

View File

@@ -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)
}

View File

@@ -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<Profile>
@Query("SELECT * FROM NostrEvent WHERE kind = 0 AND content LIKE '%' || :searchTerm || '%' LIMIT :limit")
fun filterProfiles(searchTerm: String, limit: Int = 21): List<LocalProfile>
@Query("SELECT * FROM Profile WHERE publicKey = :publicKey")
fun getProfileByPublicKey(publicKey: String): Profile?

View File

@@ -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<LocalProfile?>
fun observeProfile(publicKey: String): Flow<LocalAccount?>
@Query("SELECT * FROM UnsignedNostrEvent WHERE pubKey = :publicKey AND signedAt IS NULL") // TODO: where nostrEvent.unsignedNostrEventId is 0
fun observeUnsignedNostrEvents(publicKey: String): Flow<UnsignedNostrEvent?>

View File

@@ -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
): NostrEventEntity, TimestampedEntity, LocalStoreEntity, UserViewableEntity, BroadcastableEntity, SoftDeletableEntity {
fun humanReadableNameOrPubkey(): String {
return displayName ?: userName ?: nip05 ?: twitter ?: publicKey
}
}

View File

@@ -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<SynchronizeNostrEventRequest>,
)

View File

@@ -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<SynchronizeNostrEventRequest>,
val profile: Profile,
)

View File

@@ -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<LocalProfile?> {
override suspend fun observeProfile(publicKey: HexKey): Flow<LocalAccount?> {
return database.unsignedNostrEventDao().observeProfile(publicKey)
}
@@ -326,6 +327,10 @@ class DatabaseNostrRepository(
return database.profileDao().getAllProfiles()
}
override suspend fun filterProfiles(searchTerm: String): List<LocalProfile> {
return database.profileDao().filterProfiles(searchTerm = searchTerm)
}
override suspend fun getRecentSearches(): List<RecentSearch> {
return database.recentSearchDao().getAllRecentSearches()
}

View File

@@ -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<LocalProfile?>
suspend fun observeProfile(publicKey: HexKey): Flow<LocalAccount?>
suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<UnsignedNostrEvent?>
@@ -75,6 +76,8 @@ interface NostrRepository {
suspend fun searchableProfiles(): List<Profile>
suspend fun filterProfiles(searchTerm: String): List<LocalProfile>
suspend fun getRecentSearches(): List<RecentSearch>
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<LocalProfile?> {
override suspend fun observeProfile(publicKey: HexKey): Flow<LocalAccount?> {
TODO("Not yet implemented")
}
@@ -173,6 +176,10 @@ interface NostrRepository {
TODO("Not yet implemented")
}
override suspend fun filterProfiles(searchTerm: String): List<LocalProfile> {
TODO("Not yet implemented")
}
override suspend fun getRecentSearches(): List<RecentSearch> {
TODO("Not yet implemented")
}

View File

@@ -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 {

View File

@@ -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()
}

View File

@@ -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(),

View File

@@ -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<Profile>()
private set
var filteredProfiles = mutableStateListOf<Profile>()
private set
var recentSearches = mutableStateListOf<RecentSearch>()
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(