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 fa4c8a23..ab859be1 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 @@ -3,77 +3,54 @@ package ac.aux.compose.ui.composable import ac.aux.compose.repository.NostrRepository import ac.aux.compose.repository.SearchRepository import ac.aux.compose.ui.theme.AuxTheme -import ac.aux.compose.ui.view.model.FeedListViewModel -import ac.aux.compose.ui.view.model.SearchViewModel -import ac.aux.compose.ui.view.state.SearchUIState -import androidx.compose.foundation.clickable +import ac.aux.compose.ui.view.model.SearchResultType +import ac.aux.compose.ui.view.model.SearchResultViewModel import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew -import androidx.compose.material.icons.filled.DocumentScanner -import androidx.compose.material.icons.filled.ManageSearch -import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.SettingsInputComponent -import androidx.compose.material.icons.filled.Tag -import androidx.compose.material.icons.filled.TravelExplore import androidx.compose.material.icons.filled.Tune -import androidx.compose.material.icons.filled.YoutubeSearchedFor -import androidx.compose.material3.AppBarWithSearch -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonColors -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExpandedFullScreenSearchBar import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Surface +import androidx.compose.material3.Tab import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopSearchBar -import androidx.compose.material3.rememberSearchBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.traversalIndex -import androidx.compose.ui.text.style.TextAlign +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 -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun SearchResultScreen( searchQuery: String, @@ -81,9 +58,9 @@ fun SearchResultScreen( searchRepository: SearchRepository, onNavigateBack: () -> Unit, ) { - val searchViewModel: SearchViewModel = viewModel( - factory = SearchViewModel.factory( - initialSearchUIState = SearchUIState.Prompt, + val searchViewModel: SearchResultViewModel = viewModel( + factory = SearchResultViewModel.factory( + initialSearchResultType = SearchResultType.Posts, nostrRepository = nostrRepository, searchRepository = searchRepository ), @@ -146,7 +123,6 @@ fun SearchResultScreen( searchViewModel.performSearch( textFieldState.text.toString() ) - expanded = false }, enabled = false, expanded = expanded, @@ -155,6 +131,10 @@ fun SearchResultScreen( leadingIcon = { if (expanded) { IconButton( + enabled = true, + colors = IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.primary + ), onClick = { // TODO: onNavigateBack onNavigateBack.invoke() @@ -175,6 +155,9 @@ fun SearchResultScreen( }, trailingIcon = { IconButton( + colors = IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.primary + ), onClick = { // TODO: Adjust search } @@ -193,49 +176,39 @@ fun SearchResultScreen( }, windowInsets = TopAppBarDefaults.windowInsets ) { - // TODO: Tabs - LazyColumn { - itemsIndexed(searchResults) { index, item -> - ListItem( - headlineContent = { Text(item) }, - supportingContent = { - if (index == 0) { - Text( - text = "Search" - ) - } else if (index == 1) { - Text( - text = "Search hashtags" - ) - } else { - Text("Profile") - } + PrimaryScrollableTabRow( + modifier = Modifier, + edgePadding = 10.dp, + selectedTabIndex = searchViewModel.searchResultType.ordinal, + ) { + SearchResultType.entries.forEach { searchResultType -> + Tab( + selected = searchViewModel.searchResultType == searchResultType, + onClick = { + searchViewModel.updateSearchResultType(searchResultType) }, - leadingContent = { - if (index == 0) { - Icon( - Icons.Default.TravelExplore, - contentDescription = "Search globally" - ) - } else if (index == 1) { - Icon( - Icons.Default.Tag, - contentDescription = "Search hashtags" - ) - } else { - Icon( - Icons.Default.Person, - contentDescription = "Person" - ) - } - }, - modifier = Modifier - .clickable { + text = { + Text( + text = searchResultType.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + ) + } + } - } - .fillMaxWidth(), + LazyColumn( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + item { + Spacer( + modifier = Modifier.height(50.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 63c4213b..c6a8730d 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 @@ -29,6 +29,7 @@ import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.ManageSearch import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Search @@ -59,6 +60,7 @@ import androidx.compose.material3.TopSearchBar import androidx.compose.material3.rememberSearchBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -118,35 +120,38 @@ fun SearchScreen( "Zinfandel", ) - val recentSearches = listOf( - "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 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", @@ -177,7 +182,7 @@ fun SearchScreen( ) val textFieldState = rememberTextFieldState() // Controls expansion state of the search bar - var expanded by rememberSaveable { mutableStateOf(false) } + var expanded by rememberSaveable { mutableStateOf(true) } Scaffold { innerPadding -> Column( @@ -242,68 +247,101 @@ fun SearchScreen( if (textFieldState.text.isNotBlank()) { // Display search results in a scrollable column LazyColumn { + item { + ListItem( + headlineContent = { + Text( + textFieldState.text.toString() + ) + }, + supportingContent = { + Text( + text = "Search" + ) + }, + leadingContent = { + IconButton( + onClick = { + onNavigateToSearchResult.invoke( + SearchResultRoute( + textFieldState.text.toString() + ) + ) + } + ) { + Icon( + Icons.Default.TravelExplore, + contentDescription = "Search globally" + ) + } + }, + modifier = Modifier + .clickable { + onNavigateToSearchResult.invoke( + SearchResultRoute( + textFieldState.text.toString() + ) + ) + } + .fillMaxWidth(), + + ) + } + + item { + ListItem( + headlineContent = { Text(textFieldState.text.toString()) }, + supportingContent = { + Text( + text = "Search hashtags" + ) + }, + leadingContent = { + IconButton( + onClick = { + onNavigateToSearchResult.invoke( + SearchResultRoute( + query = "#${textFieldState.text.toString().replace("#","")}" + ) + ) + } + ) { + Icon( + Icons.Default.Tag, + contentDescription = "Search hashtags" + ) + } + }, + modifier = Modifier + .clickable { + onNavigateToSearchResult.invoke( + SearchResultRoute( + "#${textFieldState.text.toString().replaceFirst("#","")}" + ) + ) + } + .fillMaxWidth(), + + ) + } itemsIndexed(searchResults) { index, item -> ListItem( headlineContent = { Text(item) }, supportingContent = { - if (index == 0) { - Text( - text = "Search" - ) - } else if (index == 1) { - Text( - text = "Search hashtags" - ) - } else { - Text("Profile") - } + Text("Profile") }, leadingContent = { - if (index == 0) { - IconButton( - onClick = { - onNavigateToSearchResult.invoke( - SearchResultRoute( - textFieldState.text.toString() - ) - ) - } - ) { - Icon( - Icons.Default.TravelExplore, - contentDescription = "Search globally" - ) - } - - } else if (index == 1) { - IconButton( - onClick = { - onNavigateToSearchResult.invoke( - SearchResultRoute( - query = "#${textFieldState.text.toString().replace("#","")}" - ) - ) - } - ) { - Icon( - Icons.Default.Tag, - contentDescription = "Search hashtags" - ) - } - - } else { - IconButton( - onClick = { - onNavigateToProfile.invoke( - ImplementationPendingRoute("Profile result") - ) - } - ) { - Icon( - Icons.Default.Person, - contentDescription = "Person" + IconButton( + onClick = { + onNavigateToProfile.invoke( + ImplementationPendingRoute("Profile result") ) } + ) { + Icon( + Icons.Default.Person, + contentDescription = "Person" + ) } }, modifier = Modifier @@ -316,7 +354,7 @@ fun SearchScreen( } .fillMaxWidth(), - ) + ) } } } else { @@ -398,6 +436,18 @@ fun SearchScreen( ) } .fillMaxWidth(), + trailingContent = { + IconButton( + onClick = { + recentSearches.removeAt(index) + } + ) { + Icon( + Icons.Default.Close, + contentDescription = "Remove search term" + ) + } + } ) } } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/detail/MetadataEventDetail.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/detail/MetadataEventDetail.kt index c50b86eb..7a0fde8a 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/detail/MetadataEventDetail.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/detail/MetadataEventDetail.kt @@ -34,6 +34,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -46,6 +47,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -169,22 +171,23 @@ fun MetadataEventDetail( } item { - val destinations = arrayOf("Notes", "Follows", "Following") - val navController = rememberNavController() + val destinations = arrayOf("Notes", "Replies", "Articles", "Follows", "Following", "Photos", "Videos") + val startDestination = 0 - val selectedDestination by rememberSaveable { + var selectedDestination by rememberSaveable { mutableIntStateOf(startDestination) } - PrimaryTabRow( + PrimaryScrollableTabRow( modifier = Modifier.padding(10.dp), + edgePadding = 10.dp, selectedTabIndex = selectedDestination, ) { destinations.forEachIndexed { index, destination -> Tab( selected = selectedDestination == index, onClick = { - + selectedDestination = index }, text = { Text( 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 new file mode 100755 index 00000000..11dfed9e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/SearchResultViewModel.kt @@ -0,0 +1,90 @@ +package ac.aux.compose.ui.view.model + +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 androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +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 + +enum class SearchResultType { + Posts, Articles, Profiles, Photos, Videos +} + +class SearchResultViewModel( + initialSearchResultType: SearchResultType, + val nostrRepository: NostrRepository, + val searchRepository: SearchRepository, +): ViewModel() { + val logger = Logger.withTag(TAG) + + var searchResultType: SearchResultType by mutableStateOf(initialSearchResultType) + private set + + + fun updateSearchResultType (searchResultType: SearchResultType) { + this.searchResultType = searchResultType + + // TODO: Reload search results... + } + + 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 + ) + } + ) + } + } + + + companion object { + const val TAG = "SearchViewModel" + + fun factory( + initialSearchResultType: SearchResultType = SearchResultType.Posts, + nostrRepository: NostrRepository, + searchRepository: SearchRepository, + ): ViewModelProvider.Factory = viewModelFactory { + initializer { + SearchResultViewModel( + initialSearchResultType = initialSearchResultType, + nostrRepository = nostrRepository, + searchRepository = searchRepository + ) + } + } + } +} \ No newline at end of file