diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt index 3aaf8dcb5..a021dfd78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt @@ -30,12 +30,16 @@ import androidx.compose.ui.focus.FocusRequester import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.search.SearchScope +import com.vitorpamplona.amethyst.commons.search.SearchSortOrder +import com.vitorpamplona.amethyst.commons.search.SearchSource import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.userUriPrefixes import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -45,6 +49,9 @@ import com.vitorpamplona.quartz.nip05DnsIdentifiers.INip05Client import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id import com.vitorpamplona.quartz.nip10Notes.content.findHashtags import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NSec @@ -79,6 +86,11 @@ class SearchBarViewModel( val invalidations = MutableStateFlow(0) val searchValueFlow = MutableStateFlow("") + val scope = MutableStateFlow(SearchScope.ALL) + val source = MutableStateFlow(SearchSource.RELAYS) + val followsOnly = MutableStateFlow(false) + val sortOrder = MutableStateFlow(SearchSortOrder.EVENT_DEFAULT) + val searchTerm = searchValueFlow .debounce(300) @@ -88,6 +100,12 @@ class SearchBarViewModel( val searchDataSourceState = SearchQueryState(MutableStateFlow(searchValue), account) + @Suppress("unused") + val sourceWatcher = + source + .onEach { updateDataSource(searchValue) } + .stateIn(viewModelScope, SharingStarted.Eagerly, SearchSource.RELAYS) + val listState: LazyListState = LazyListState(0, 0) @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @@ -152,21 +170,45 @@ class SearchBarViewModel( } }.flowOn(Dispatchers.IO) + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + val directEventResolver: Flow = + searchTerm + .debounce(200) + .mapLatest { term -> + if (term.isBlank()) return@mapLatest null + runCatching { + when (val parsed = Nip19Parser.uriToRoute(term)?.entity) { + is NEvent -> Route.Note(parsed.hex) + is NNote -> Route.Note(parsed.hex) + is NAddress -> Route.Note(parsed.aTag()) + else -> null + } + }.getOrNull() + }.flowOn(Dispatchers.IO) + val searchResultsUsers = combine( searchValueFlow.debounce(100), invalidations.debounce(100), directNip05Resolver, - ) { term, version, nip05Resolver -> + scope, + combine(followsOnly, account.kind3FollowList.flow) { only, follows -> + if (only) follows.authorsPlusMe else null + }, + ) { term, _, nip05Resolver, currentScope, follows -> + if (currentScope == SearchScope.NOTES) return@combine emptyList() + if (nip05Resolver != null) { - return@combine listOf(nip05Resolver) + return@combine if (follows == null || nip05Resolver.pubkeyHex in follows) { + listOf(nip05Resolver) + } else { + emptyList() + } } - if (term.isNotBlank()) { - LocalCache.findUsersStartingWith(term, account) - } else { - emptyList() - } + if (term.isBlank()) return@combine emptyList() + val users = LocalCache.findUsersStartingWith(term, account) + if (follows != null) users.filter { it.pubkeyHex in follows } else users }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) @@ -174,10 +216,37 @@ class SearchBarViewModel( combine( searchValueFlow.debounce(100), invalidations, - ) { term, version -> - LocalCache - .findNotesStartingWith(term, account.hiddenUsers) - .sortedWith(DefaultFeedOrder) + scope, + sortOrder, + combine(followsOnly, account.kind3FollowList.flow) { only, follows -> + if (only) follows.authorsPlusMe else null + }, + ) { term, _, currentScope, order, follows -> + if (currentScope == SearchScope.PEOPLE) return@combine emptyList() + + val raw = LocalCache.findNotesStartingWith(term, account.hiddenUsers) + val filtered = if (follows != null) raw.filter { it.author?.pubkeyHex in follows } else raw + + when (order) { + SearchSortOrder.POPULAR -> { + filtered.sortedWith( + compareByDescending { it.zapsAmount } + .thenByDescending { it.createdAt() ?: 0L }, + ) + } + + SearchSortOrder.OLDEST -> { + filtered.sortedBy { it.createdAt() ?: 0L } + } + + SearchSortOrder.RELEVANCE, SearchSortOrder.NEWEST -> { + filtered.sortedWith(DefaultFeedOrder) + } + + else -> { + filtered.sortedWith(DefaultFeedOrder) + } + } }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) @@ -185,8 +254,9 @@ class SearchBarViewModel( combine( searchValueFlow.debounce(100), invalidations, - ) { term, version -> - LocalCache.findPublicChatChannelsStartingWith(term) + scope, + ) { term, _, currentScope -> + if (currentScope != SearchScope.ALL) emptyList() else LocalCache.findPublicChatChannelsStartingWith(term) }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) @@ -194,8 +264,9 @@ class SearchBarViewModel( combine( searchValueFlow.debounce(100), invalidations, - ) { term, version -> - LocalCache.findEphemeralChatChannelsStartingWith(term) + scope, + ) { term, _, currentScope -> + if (currentScope != SearchScope.ALL) emptyList() else LocalCache.findEphemeralChatChannelsStartingWith(term) }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) @@ -203,8 +274,9 @@ class SearchBarViewModel( combine( searchValueFlow.debounce(100), invalidations, - ) { term, version -> - LocalCache.findLiveActivityChannelsStartingWith(term) + scope, + ) { term, _, currentScope -> + if (currentScope != SearchScope.ALL) emptyList() else LocalCache.findLiveActivityChannelsStartingWith(term) }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) @@ -212,8 +284,9 @@ class SearchBarViewModel( combine( searchValueFlow.debounce(100), invalidations, - ) { term, version -> - findHashtags(term) + scope, + ) { term, _, currentScope -> + if (currentScope == SearchScope.PEOPLE) emptyList() else findHashtags(term) }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) @@ -221,7 +294,9 @@ class SearchBarViewModel( combine( searchValueFlow.debounce(100), invalidations, - ) { term, version -> + scope, + ) { term, _, currentScope -> + if (currentScope != SearchScope.ALL) return@combine emptyList() if (term.length > 1) { val isTypingRelay = term.length > 7 && (term.startsWith("wss://") || term.startsWith("ws://")) val relayUrl = @@ -263,7 +338,7 @@ class SearchBarViewModel( fun clear() = updateSearchValue("") suspend fun updateDataSource(searchTerm: String) { - if (searchTerm.isBlank()) { + if (searchTerm.isBlank() || source.value == SearchSource.LOCAL) { searchDataSourceState.searchQuery.tryEmit("") } else { searchDataSourceState.searchQuery.tryEmit(searchTerm) @@ -271,6 +346,22 @@ class SearchBarViewModel( } } + fun updateScope(newScope: SearchScope) { + scope.value = newScope + } + + fun updateSource(newSource: SearchSource) { + source.value = newSource + } + + fun updateFollowsOnly(value: Boolean) { + followsOnly.value = value + } + + fun updateSortOrder(order: SearchSortOrder) { + sortOrder.value = order + } + fun isSearchingFun() = searchValue.isNotBlank() class Factory( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index 09c7eaa50..6d2a9cad7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -20,27 +20,49 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.search +import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Tune +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.RadioButton +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.focusRequester @@ -52,6 +74,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.search.SearchScope +import com.vitorpamplona.amethyst.commons.search.SearchSortOrder +import com.vitorpamplona.amethyst.commons.search.SearchSource import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.service.relayClient.searchCommand.TextSearchDataSourceSubscription @@ -77,6 +102,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdTopPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @Composable @@ -157,11 +183,255 @@ private fun SearchBar( } } } + + // bech32 auto-resolve: navigate on hit without displaying results + launch { + searchBarViewModel.directEventResolver.filterNotNull().collect { route -> + nav.nav(route) + searchBarViewModel.clear() + } + } } - SearchTextField(searchBarViewModel, Modifier.statusBarsPadding()) + Column(modifier = Modifier.statusBarsPadding()) { + SearchTextField(searchBarViewModel, Modifier) + SearchFilterRow(searchBarViewModel) + } } +// Must match SearchBarViewModel.source's initial value. +private val DEFAULT_SOURCE = SearchSource.RELAYS + +private fun hasNonDefaultFilters( + scope: SearchScope, + source: SearchSource, + followsOnly: Boolean, + sort: SearchSortOrder, +): Boolean { + if (source != DEFAULT_SOURCE) return true + if (followsOnly) return true + if (scope != SearchScope.PEOPLE && sort != SearchSortOrder.EVENT_DEFAULT) return true + return false +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SearchFilterRow(searchBarViewModel: SearchBarViewModel) { + val currentScope by searchBarViewModel.scope.collectAsStateWithLifecycle() + val currentSource by searchBarViewModel.source.collectAsStateWithLifecycle() + val currentFollowsOnly by searchBarViewModel.followsOnly.collectAsStateWithLifecycle() + val currentSort by searchBarViewModel.sortOrder.collectAsStateWithLifecycle() + + var sheetOpen by remember { mutableStateOf(false) } + + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + SingleChoiceSegmentedButtonRow(modifier = Modifier.weight(1f)) { + val scopes = listOf(SearchScope.ALL, SearchScope.PEOPLE, SearchScope.NOTES) + scopes.forEachIndexed { index, s -> + SegmentedButton( + selected = currentScope == s, + onClick = { searchBarViewModel.updateScope(s) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = scopes.size), + ) { + Text( + text = + when (s) { + SearchScope.ALL -> stringRes(R.string.search_scope_all) + SearchScope.PEOPLE -> stringRes(R.string.search_scope_people) + SearchScope.NOTES -> stringRes(R.string.search_scope_notes) + }, + ) + } + } + } + + FilterIconButton( + hasBadge = + hasNonDefaultFilters( + scope = currentScope, + source = currentSource, + followsOnly = currentFollowsOnly, + sort = currentSort, + ), + onClick = { sheetOpen = true }, + ) + } + + if (sheetOpen) { + SearchFiltersSheet( + scope = currentScope, + source = currentSource, + followsOnly = currentFollowsOnly, + sort = currentSort, + onSourceChange = searchBarViewModel::updateSource, + onFollowsOnlyChange = searchBarViewModel::updateFollowsOnly, + onSortChange = searchBarViewModel::updateSortOrder, + onReset = { + searchBarViewModel.updateSource(DEFAULT_SOURCE) + searchBarViewModel.updateFollowsOnly(false) + searchBarViewModel.updateSortOrder(SearchSortOrder.EVENT_DEFAULT) + }, + onDismiss = { sheetOpen = false }, + ) + } +} + +@Composable +private fun FilterIconButton( + hasBadge: Boolean, + onClick: () -> Unit, +) { + Box { + IconButton(onClick = onClick) { + Icon( + imageVector = Icons.Outlined.Tune, + contentDescription = stringRes(R.string.search_filters_open), + ) + } + if (hasBadge) { + Box( + modifier = + Modifier + .align(Alignment.TopEnd) + .offset(x = (-10).dp, y = 10.dp) + .size(8.dp) + .background(MaterialTheme.colorScheme.primary, CircleShape), + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SearchFiltersSheet( + scope: SearchScope, + source: SearchSource, + followsOnly: Boolean, + sort: SearchSortOrder, + onSourceChange: (SearchSource) -> Unit, + onFollowsOnlyChange: (Boolean) -> Unit, + onSortChange: (SearchSortOrder) -> Unit, + onReset: () -> Unit, + onDismiss: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .padding(bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Text( + text = stringRes(R.string.search_filters_title), + style = MaterialTheme.typography.titleMedium, + ) + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = stringRes(R.string.search_filters_section_source), + style = MaterialTheme.typography.labelLarge, + ) + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + val sources = listOf(SearchSource.LOCAL, SearchSource.RELAYS) + sources.forEachIndexed { index, s -> + SegmentedButton( + selected = source == s, + onClick = { onSourceChange(s) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = sources.size), + ) { + Text( + text = + when (s) { + SearchSource.LOCAL -> stringRes(R.string.search_source_local) + SearchSource.RELAYS -> stringRes(R.string.search_source_relays) + }, + ) + } + } + } + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { onFollowsOnlyChange(!followsOnly) } + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringRes(R.string.search_follows_only), + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodyLarge, + ) + Switch( + checked = followsOnly, + onCheckedChange = onFollowsOnlyChange, + ) + } + + if (scope != SearchScope.PEOPLE) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = stringRes(R.string.search_filters_section_sort), + style = MaterialTheme.typography.labelLarge, + ) + SearchSortOrder.EVENT_OPTIONS.forEach { opt -> + Row( + modifier = + Modifier + .fillMaxWidth() + .selectable( + selected = sort == opt, + onClick = { onSortChange(opt) }, + ).padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = sort == opt, + onClick = { onSortChange(opt) }, + ) + Text( + text = sortLabel(opt), + modifier = Modifier.padding(start = 8.dp), + style = MaterialTheme.typography.bodyLarge, + ) + } + } + } + } + + TextButton(onClick = onReset) { + Text(stringRes(R.string.search_filters_reset)) + } + } + } +} + +@Composable +private fun sortLabel(opt: SearchSortOrder): String = + when (opt) { + SearchSortOrder.NEWEST -> stringRes(R.string.search_sort_newest) + SearchSortOrder.RELEVANCE -> stringRes(R.string.search_sort_relevance) + SearchSortOrder.POPULAR -> stringRes(R.string.search_sort_popular) + SearchSortOrder.OLDEST -> stringRes(R.string.search_sort_oldest) + SearchSortOrder.NAME_AZ, SearchSortOrder.NAME_ZA -> opt.label + } + @Composable private fun SearchTextField( searchBarViewModel: SearchBarViewModel, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 182e677bb..ac4486157 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1870,6 +1870,22 @@ Search hashtag: #%1$s + All + People + Notes + Local + Relays + Follows only + Newest + Oldest + Relevance + Popular + Filters + Reset + Source + Sort by + Filters + Don\'t Translate From Languages shown here will not be translated. Select a language to remove it and have it translated again. Translate To diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt index 88702439c..44b16a601 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt @@ -84,10 +84,10 @@ class AdvancedSearchBarState( val noteResults: StateFlow> = _noteResults.asStateFlow() // Sort orders - private val _eventSortOrder = MutableStateFlow(SearchSortOrder.DEFAULT_EVENT) + private val _eventSortOrder = MutableStateFlow(SearchSortOrder.EVENT_DEFAULT) val eventSortOrder: StateFlow = _eventSortOrder.asStateFlow() - private val _peopleSortOrder = MutableStateFlow(SearchSortOrder.DEFAULT_PEOPLE) + private val _peopleSortOrder = MutableStateFlow(SearchSortOrder.PEOPLE_DEFAULT) val peopleSortOrder: StateFlow = _peopleSortOrder.asStateFlow() // Derived sorted results @@ -266,8 +266,8 @@ class AdvancedSearchBarState( _peopleResults.value = persistentListOf() _noteResults.value = persistentListOf() _relayStates.value = persistentListOf() - _eventSortOrder.value = SearchSortOrder.DEFAULT_EVENT - _peopleSortOrder.value = SearchSortOrder.DEFAULT_PEOPLE + _eventSortOrder.value = SearchSortOrder.EVENT_DEFAULT + _peopleSortOrder.value = SearchSortOrder.PEOPLE_DEFAULT activeSubIds.value = emptySet() eventDeduplicator.clear() } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt index 04e92ebec..188f6de56 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt @@ -48,6 +48,12 @@ object SearchResultSorter { } } + SearchSortOrder.POPULAR -> { + // Raw Event has no zap-total; callers that hold Note objects should sort by + // zapsAmount directly. Fall back to newest so the option is still harmless here. + events.sortedWith(compareByDescending { it.createdAt }.thenBy { it.id }) + } + else -> { events } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchScope.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchScope.kt new file mode 100644 index 000000000..68b0b89b2 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchScope.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +enum class SearchScope { + ALL, + PEOPLE, + NOTES, +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSortOrder.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSortOrder.kt index 1b4350787..283af1dbf 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSortOrder.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSortOrder.kt @@ -26,14 +26,15 @@ enum class SearchSortOrder( NEWEST("Newest"), OLDEST("Oldest"), RELEVANCE("Relevance"), + POPULAR("Popular"), NAME_AZ("A → Z"), NAME_ZA("Z → A"), ; companion object { - val EVENT_OPTIONS = listOf(NEWEST, OLDEST, RELEVANCE) + val EVENT_OPTIONS = listOf(RELEVANCE, NEWEST, POPULAR) val PEOPLE_OPTIONS = listOf(NAME_AZ, NAME_ZA) - val DEFAULT_EVENT = NEWEST - val DEFAULT_PEOPLE = NAME_AZ + val EVENT_DEFAULT = NEWEST + val PEOPLE_DEFAULT = NAME_AZ } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSource.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSource.kt new file mode 100644 index 000000000..404e37dc0 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSource.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +enum class SearchSource { + LOCAL, + RELAYS, +}