Merge pull request #2522 from davotoula/improve-search-toggles

Improve search toggles
This commit is contained in:
Vitor Pamplona
2026-04-23 08:48:12 -04:00
committed by GitHub
8 changed files with 466 additions and 29 deletions
@@ -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<Route?> =
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<User>()
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<User>()
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<com.vitorpamplona.amethyst.model.Note> { 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(
@@ -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,
+16
View File
@@ -1870,6 +1870,22 @@
<string name="search_by_hashtag">Search hashtag: #%1$s</string>
<string name="search_scope_all">All</string>
<string name="search_scope_people">People</string>
<string name="search_scope_notes">Notes</string>
<string name="search_source_local">Local</string>
<string name="search_source_relays">Relays</string>
<string name="search_follows_only">Follows only</string>
<string name="search_sort_newest">Newest</string>
<string name="search_sort_oldest">Oldest</string>
<string name="search_sort_relevance">Relevance</string>
<string name="search_sort_popular">Popular</string>
<string name="search_filters_title">Filters</string>
<string name="search_filters_reset">Reset</string>
<string name="search_filters_section_source">Source</string>
<string name="search_filters_section_sort">Sort by</string>
<string name="search_filters_open">Filters</string>
<string name="dont_translate_from">Don\'t Translate From</string>
<string name="dont_translate_from_description">Languages shown here will not be translated. Select a language to remove it and have it translated again.</string>
<string name="translate_to">Translate To</string>
@@ -84,10 +84,10 @@ class AdvancedSearchBarState(
val noteResults: StateFlow<ImmutableList<Event>> = _noteResults.asStateFlow()
// Sort orders
private val _eventSortOrder = MutableStateFlow(SearchSortOrder.DEFAULT_EVENT)
private val _eventSortOrder = MutableStateFlow(SearchSortOrder.EVENT_DEFAULT)
val eventSortOrder: StateFlow<SearchSortOrder> = _eventSortOrder.asStateFlow()
private val _peopleSortOrder = MutableStateFlow(SearchSortOrder.DEFAULT_PEOPLE)
private val _peopleSortOrder = MutableStateFlow(SearchSortOrder.PEOPLE_DEFAULT)
val peopleSortOrder: StateFlow<SearchSortOrder> = _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()
}
@@ -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<Event> { it.createdAt }.thenBy { it.id })
}
else -> {
events
}
@@ -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,
}
@@ -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
}
}
@@ -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,
}