From 9358aa07a860e74bc6ccc852003e09188dcf0771 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Sun, 19 Apr 2026 23:15:09 +0200 Subject: [PATCH] Actually perform search --- .../ui/composable/SearchResultScreen.kt | 200 ++++++++++-------- .../ui/composable/navigation/AuxNavHost.kt | 2 +- .../ui/view/model/SearchResultViewModel.kt | 115 +++++++--- 3 files changed, 203 insertions(+), 114 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchResultScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchResultScreen.kt index dc7decd7..9b2b9cbc 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchResultScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchResultScreen.kt @@ -2,7 +2,6 @@ package ac.aux.compose.ui.composable import ac.aux.compose.repository.NostrRepository import ac.aux.compose.repository.SearchRepository -import ac.aux.compose.ui.composable.navigation.routes.ImplementationPendingRoute import ac.aux.compose.ui.composable.navigation.routes.NostrEventDetailRoute import ac.aux.compose.ui.composable.navigation.routes.Route import ac.aux.compose.ui.composable.widgets.feed.ListView @@ -20,6 +19,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew @@ -43,8 +44,8 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -57,6 +58,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -67,6 +69,8 @@ fun SearchResultScreen( onNavigateBack: () -> Unit, onNavigateToEvent: (Route) -> Unit ) { + val scope = rememberCoroutineScope() + val searchViewModel: SearchResultViewModel = viewModel( factory = SearchResultViewModel.factory( initialSearchResultType = SearchResultType.Posts, @@ -76,33 +80,6 @@ fun SearchResultScreen( ), ) - 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(searchQuery) // Controls expansion state of the search bar var expanded by rememberSaveable { mutableStateOf(true) } @@ -184,6 +161,8 @@ fun SearchResultScreen( }, windowInsets = TopAppBarDefaults.windowInsets ) { + val pagerState = rememberPagerState { SearchResultType.entries.size } + PrimaryScrollableTabRow( modifier = Modifier, edgePadding = 10.dp, @@ -194,6 +173,11 @@ fun SearchResultScreen( selected = searchViewModel.searchResultType == searchResultType, onClick = { searchViewModel.updateSearchResultType(searchResultType) + scope.launch { + pagerState.animateScrollToPage( + searchResultType.ordinal + ) + } }, text = { Text( @@ -206,22 +190,90 @@ fun SearchResultScreen( } } - when (val searchResultListUIState = searchViewModel.searchResultListUIState) { - SearchResultListUIState.Error -> { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer( - modifier = Modifier.height(50.dp) - ) - Text( - text = "Something went wrong", - ) + + + + HorizontalPager( + state = pagerState + ) { pageIndex -> + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + when(SearchResultType.entries[pageIndex]) { + SearchResultType.Posts -> { + Text("Posts") + } + SearchResultType.Articles -> { + Text("Posts") + } + SearchResultType.Profiles -> { + Text("Posts") + } + SearchResultType.Photos -> { + + Text("Posts") + } + SearchResultType.Videos -> { + + Text("Posts") + } } - } - is SearchResultListUIState.Loaded -> { - if (searchResultListUIState.localNostrEvents.isEmpty()) { + + when (val searchResultListUIState = searchViewModel.searchResultListUIState) { + SearchResultListUIState.Error -> { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer( + modifier = Modifier.height(50.dp) + ) + Text( + text = "Something went wrong", + ) + } + } + is SearchResultListUIState.Loaded -> { + if (searchResultListUIState.localNostrEvents.isEmpty()) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer( + modifier = Modifier.height(50.dp) + ) + Text( + text = "No events were found", + ) + } + } else { + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items( + items = searchResultListUIState.localNostrEvents, + key = { localNostrEvent -> localNostrEvent.nostrEvent.id} + ) { localNostrEvent -> + localNostrEvent.ListView( + onNavigateToEvent = { nostrEventId -> + onNavigateToEvent.invoke( + NostrEventDetailRoute( + nostrEventId = nostrEventId + ) + ) + } + ) + } + } + } + + } + SearchResultListUIState.Loading -> { Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally @@ -230,59 +282,29 @@ fun SearchResultScreen( modifier = Modifier.height(50.dp) ) Text( - text = "No events were found", + text = "Searching for ${searchViewModel.searchResultType.name.lowercase()} on \"${searchQuery.lowercase()}\"", + textAlign = TextAlign.Center ) - } - } else { - LazyColumn( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - items( - items = searchResultListUIState.localNostrEvents, - key = { localNostrEvent -> localNostrEvent.nostrEvent.id} - ) { localNostrEvent -> - localNostrEvent.ListView( - onNavigateToEvent = { nostrEventId -> - onNavigateToEvent.invoke( - NostrEventDetailRoute( - nostrEventId = nostrEventId - ) - ) - } - ) - } + Spacer( + modifier = Modifier.height(50.dp) + ) + + LoadingIndicator() } } - } - SearchResultListUIState.Loading -> { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer( - modifier = Modifier.height(50.dp) - ) - Text( - text = "Searching for ${searchViewModel.searchResultType.name.lowercase()} on \"${searchQuery.lowercase()}\"", - textAlign = TextAlign.Center - ) - Spacer( - modifier = Modifier.height(50.dp) - ) - - LoadingIndicator() - } - - LaunchedEffect(true) { - searchViewModel.loadSearchResult() - } } + + + + + } + + + } } } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt index 9b304b54..9493a208 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt @@ -78,7 +78,7 @@ fun AuxNavHost( database = databaseManager.auxDatabase ) val searchRepository = DatabaseSearchRepository( - database = databaseManager.inMemoryAuxDatabase + database = databaseManager.auxDatabase ) val exceptionHandler = diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/SearchResultViewModel.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/SearchResultViewModel.kt index 29779c10..b0c6d221 100755 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/SearchResultViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/SearchResultViewModel.kt @@ -5,7 +5,6 @@ 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.FeedListUIState import ac.aux.compose.ui.view.state.SearchResultListUIState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -16,8 +15,10 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import co.touchlab.kermit.Logger +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import kotlinx.coroutines.Dispatchers @@ -43,45 +44,98 @@ class SearchResultViewModel( private set fun updateSearchResultType (searchResultType: SearchResultType) { + searchResultListUIState = SearchResultListUIState.Loading + this.searchResultType = searchResultType - reloadSearchResult() + initiateSearchResults(searchResultType) } - fun reloadSearchResult() { - searchResultListUIState = SearchResultListUIState.Loading + init { + initiateSearchResults(searchResultType) } - fun loadSearchResult() { + fun initiateSearchResults(searchResultType: SearchResultType) { + val synchronizationFilter = when (searchResultType) { + SearchResultType.Posts -> { + SynchronizationFilter( + kinds = arrayOf( + TextNoteEvent.KIND, + RepostEvent.KIND, + ReactionEvent.KIND, + LnZapEvent.KIND, + ), + search = searchQuery, + limit = 50 + ) + } + SearchResultType.Profiles -> { + SynchronizationFilter( + kinds = arrayOf( + MetadataEvent.KIND, + ), + search = searchQuery, + limit = 50 + ) + } + SearchResultType.Articles -> { + SynchronizationFilter( + kinds = arrayOf( + LongTextNoteEvent.KIND, + ), + search = searchQuery, + limit = 50 + ) + } + SearchResultType.Photos -> { + SynchronizationFilter( + kinds = arrayOf( + TextNoteEvent.KIND, + ), + // TODO: filter for tags of pictures... + search = searchQuery, + limit = 50 + ) + } + SearchResultType.Videos -> { + SynchronizationFilter( + kinds = arrayOf( + TextNoteEvent.KIND, + ), + // TODO: filter for tags of videos... + search = searchQuery, + limit = 50 + ) + } + } + + saveSearchQuery(synchronizationFilter) + scheduleSearch(synchronizationFilter) + loadSearchResultFeed(synchronizationFilter) + } + + private fun saveSearchQuery(synchronizationFilter: SynchronizationFilter) { + logger.d("saveSearchQuery: $searchQuery") viewModelScope.launch(Dispatchers.IO) { nostrRepository.saveSearchQuery( searchQuery, - // TODO: synchronizationFilterArray - ) - - // This is something not taking into consideration the results we are actually looking for... - nostrRepository.observeNostrFeed().collect { localNostrEvents -> - searchResultListUIState = SearchResultListUIState.Loaded( - localNostrEvents = localNostrEvents + arrayOf( + synchronizationFilter ) - } + ) + } + } + private fun scheduleSearch(synchronizationFilter: SynchronizationFilter) { + logger.d("scheduleSearch: $searchQuery") + viewModelScope.launch(Dispatchers.IO) { // Sync Notifications... might want to also run this in the background searchRepository.submitSearch( - Relays.eventPublishRelaySet.map { normalizedRelay -> + Relays.eventFinderRelaySet.map { normalizedRelay -> SynchronizeNostrEventRequest( purpose = "search", synchronizationFilters = arrayOf( - SynchronizationFilter( - kinds = arrayOf( - TextNoteEvent.KIND, - RepostEvent.KIND, - ReactionEvent.KIND, - LnZapEvent.KIND, - ), - search = searchQuery, - limit = 50 - ) + synchronizationFilter ), relayURL = normalizedRelay.url ) @@ -90,6 +144,19 @@ class SearchResultViewModel( } } + private fun loadSearchResultFeed(synchronizationFilter: SynchronizationFilter) { + logger.d("loadSearchResultFeed: $searchQuery") + viewModelScope.launch(Dispatchers.IO) { + // This is something not taking into consideration the results we are actually looking for... + // TODO: Search local notes using the [synchronizationFilter] + nostrRepository.observeNostrFeed().collect { localNostrEvents -> + searchResultListUIState = SearchResultListUIState.Loaded( + localNostrEvents = localNostrEvents + ) + } + } + } + companion object { const val TAG = "SearchViewModel"