feat(search): add scope, source, follows, sort toggles + bech32 auto-resolve (Android)
Android search previously dumped users, notes, hashtags, relays, and 3 channel
types into a single untoggled list and silently relied on NIP-50 relay search.
This adds a second-row control strip so users can pick what they're looking for
and how results are sourced/ranked.
Commons:
- New SearchScope { ALL, PEOPLE, NOTES } and SearchSource { LOCAL, RELAYS }
- SearchSortOrder: add POPULAR; EVENT_OPTIONS now [RELEVANCE, NEWEST, POPULAR]
Android SearchBarViewModel:
- scope / source / followsOnly / sortOrder StateFlows
- searchResultsUsers + searchResultsNotes respect scope, follows, and sort
- POPULAR sorts notes by Note.zapsAmount (descending)
- Follows-only filters authors (includes replies since filter is by author)
- directEventResolver: bech32 nevent/note/naddr -> Route.Note (auto-navigate)
- updateDataSource skips relay emit when source == LOCAL
Android SearchScreen:
- Second row: [All | People | Notes] SegmentedButton + Sort dropdown
- Third row: [Local | Relays] SegmentedButton + Follows-only chip
- bech32 hit triggers nav.nav(route) and clears the search
Desktop parity deferred - Desktop already ships an advanced panel that covers
most of this (kinds/authors/dates/hashtags/language/exclude terms + sort +
bech32 direct lookup). The new enums live in commons so Desktop can adopt.
Build note: gradle 9.3.1 can't be downloaded in this environment, so Android
and Desktop compilation were not verified here.
This commit is contained in:
+106
-21
@@ -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.DEFAULT_EVENT)
|
||||
|
||||
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,31 @@ 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 +248,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 +258,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 +268,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 +278,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 +288,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 +332,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 +340,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(
|
||||
|
||||
+152
-1
@@ -32,15 +32,26 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
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 +63,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 +91,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,9 +172,145 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SortMenu(
|
||||
currentSort = currentSort,
|
||||
enabled = currentScope != SearchScope.PEOPLE,
|
||||
onSelect = { searchBarViewModel.updateSortOrder(it) },
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
SingleChoiceSegmentedButtonRow {
|
||||
val sources = listOf(SearchSource.LOCAL, SearchSource.RELAYS)
|
||||
sources.forEachIndexed { index, s ->
|
||||
SegmentedButton(
|
||||
selected = currentSource == s,
|
||||
onClick = { searchBarViewModel.updateSource(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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FilterChip(
|
||||
selected = currentFollowsOnly,
|
||||
onClick = { searchBarViewModel.updateFollowsOnly(!currentFollowsOnly) },
|
||||
label = { Text(stringRes(R.string.search_follows_only)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SortMenu(
|
||||
currentSort: SearchSortOrder,
|
||||
enabled: Boolean,
|
||||
onSelect: (SearchSortOrder) -> Unit,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val options = SearchSortOrder.EVENT_OPTIONS
|
||||
|
||||
TextButton(
|
||||
onClick = { if (enabled) expanded = true },
|
||||
enabled = enabled,
|
||||
) {
|
||||
Text(
|
||||
text =
|
||||
when (currentSort) {
|
||||
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)
|
||||
else -> stringRes(R.string.search_sort_newest)
|
||||
},
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
options.forEach { opt ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
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)
|
||||
else -> opt.label
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onSelect(opt)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -1870,6 +1870,17 @@
|
||||
|
||||
<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="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>
|
||||
|
||||
+6
@@ -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
|
||||
}
|
||||
|
||||
+27
@@ -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,
|
||||
}
|
||||
+2
-1
@@ -26,12 +26,13 @@ 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
|
||||
|
||||
+26
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user