mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
feat(settings): curated search keywords for all rows + word-prefix search matching
Add keyword blobs so settings resolve by concept/protocol name, not just title — e.g. "blossom" -> Media Servers, "audio rooms" -> Nests Servers, "negentropy" -> Event Sync, "nsec" -> Backup Keys. NIP numbers omitted by preference.
This commit is contained in:
+19
-10
@@ -52,27 +52,36 @@ data class SettingsCategory(
|
||||
val entries: List<SettingsEntry>,
|
||||
)
|
||||
|
||||
private val SEARCH_DELIMITERS = Regex("[^\\p{L}\\p{N}]+")
|
||||
|
||||
/** Lowercases and splits text into searchable words on any non-letter/digit run. */
|
||||
private fun String.searchWords(): List<String> = lowercase().split(SEARCH_DELIMITERS).filter { it.isNotEmpty() }
|
||||
|
||||
/**
|
||||
* Filters [catalog] by [query] (case-insensitive substring over category title + entry title + keywords).
|
||||
* Blank/whitespace query returns [catalog] unchanged. Categories left with no
|
||||
* matching entries are dropped. Pure — no Compose/Android — so it is unit-testable;
|
||||
* string-resource resolution is injected via [stringLookup].
|
||||
* Filters [catalog] by [query] using case-insensitive **word-prefix** matching over
|
||||
* category title + entry title + keywords: an entry matches when every word in the
|
||||
* query is the prefix of some word in that haystack (so `dark mo` matches "dark mode",
|
||||
* but `tor` does not match "his**tor**y"). Blank/whitespace query returns [catalog]
|
||||
* unchanged. Categories left with no matching entries are dropped. Pure — no
|
||||
* Compose/Android — so it is unit-testable; string-resource resolution is injected
|
||||
* via [stringLookup].
|
||||
*/
|
||||
fun filterSettings(
|
||||
catalog: List<SettingsCategory>,
|
||||
query: String,
|
||||
stringLookup: (Int) -> String,
|
||||
): List<SettingsCategory> {
|
||||
val needle = query.trim().lowercase()
|
||||
if (needle.isEmpty()) return catalog
|
||||
val terms = query.searchWords()
|
||||
if (terms.isEmpty()) return catalog
|
||||
|
||||
return catalog.mapNotNull { category ->
|
||||
val categoryTitle = stringLookup(category.titleRes)
|
||||
val categoryWords = stringLookup(category.titleRes).searchWords()
|
||||
val matched =
|
||||
category.entries.filter { entry ->
|
||||
val keywords = entry.keywordsRes?.let { stringLookup(it) } ?: ""
|
||||
val haystack = (categoryTitle + " " + stringLookup(entry.titleRes) + " " + keywords).lowercase()
|
||||
haystack.contains(needle)
|
||||
val words =
|
||||
categoryWords + stringLookup(entry.titleRes).searchWords() +
|
||||
entry.keywordsRes?.let { stringLookup(it).searchWords() }.orEmpty()
|
||||
terms.all { term -> words.any { it.startsWith(term) } }
|
||||
}
|
||||
if (matched.isEmpty()) null else category.copy(entries = matched)
|
||||
}
|
||||
|
||||
+128
-23
@@ -47,27 +47,71 @@ fun buildSettingsCatalog(
|
||||
icon = SettingsIcon.Painter(R.drawable.relays, 4),
|
||||
keywordsRes = R.string.relay_setup_search_keywords,
|
||||
) { nav.nav(Route.EditRelays) },
|
||||
SettingsEntry(R.string.event_sync_title, SettingsIcon.Symbol(MaterialSymbols.Sync)) { nav.nav(Route.EventSync) },
|
||||
SettingsEntry(R.string.route_import_follows, SettingsIcon.Symbol(MaterialSymbols.GroupAdd)) { nav.nav(Route.ImportFollowsSelectUser) },
|
||||
SettingsEntry(R.string.media_servers, SettingsIcon.Symbol(MaterialSymbols.CloudUpload)) { nav.nav(Route.EditMediaServers) },
|
||||
SettingsEntry(R.string.nests_servers_title, SettingsIcon.Symbol(MaterialSymbols.CloudUpload)) { nav.nav(Route.EditNestsServers) },
|
||||
SettingsEntry(R.string.profile_badges_title, SettingsIcon.Symbol(MaterialSymbols.MilitaryTech)) { nav.nav(Route.ProfileBadges) },
|
||||
SettingsEntry(R.string.favorite_dvms_title, SettingsIcon.Symbol(MaterialSymbols.AutoAwesome)) { nav.nav(Route.EditFavoriteAlgoFeeds) },
|
||||
SettingsEntry(R.string.reactions, SettingsIcon.Symbol(MaterialSymbols.FavoriteBorder)) { nav.nav(Route.UpdateReactionType) },
|
||||
SettingsEntry(R.string.video_player_settings, SettingsIcon.Symbol(MaterialSymbols.VideoSettings)) { nav.nav(Route.VideoPlayerSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.event_sync_title,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Sync),
|
||||
keywordsRes = R.string.event_sync_search_keywords,
|
||||
) { nav.nav(Route.EventSync) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.route_import_follows,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.GroupAdd),
|
||||
keywordsRes = R.string.import_follows_search_keywords,
|
||||
) { nav.nav(Route.ImportFollowsSelectUser) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.media_servers,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.CloudUpload),
|
||||
keywordsRes = R.string.media_servers_search_keywords,
|
||||
) { nav.nav(Route.EditMediaServers) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.nests_servers_title,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.CloudUpload),
|
||||
keywordsRes = R.string.nests_servers_search_keywords,
|
||||
) { nav.nav(Route.EditNestsServers) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.profile_badges_title,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.MilitaryTech),
|
||||
keywordsRes = R.string.profile_badges_search_keywords,
|
||||
) { nav.nav(Route.ProfileBadges) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.favorite_dvms_title,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.AutoAwesome),
|
||||
keywordsRes = R.string.favorite_dvms_search_keywords,
|
||||
) { nav.nav(Route.EditFavoriteAlgoFeeds) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.reactions,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.FavoriteBorder),
|
||||
keywordsRes = R.string.reactions_search_keywords,
|
||||
) { nav.nav(Route.UpdateReactionType) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.video_player_settings,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.VideoSettings),
|
||||
keywordsRes = R.string.video_player_search_keywords,
|
||||
) { nav.nav(Route.VideoPlayerSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.zaps,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Bolt),
|
||||
keywordsRes = R.string.zaps_search_keywords,
|
||||
) { nav.nav(Route.UpdateZapAmount()) },
|
||||
SettingsEntry(R.string.payment_targets, SettingsIcon.Symbol(MaterialSymbols.Payment)) { nav.nav(Route.EditPaymentTargets) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.payment_targets,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Payment),
|
||||
keywordsRes = R.string.payment_targets_search_keywords,
|
||||
) { nav.nav(Route.EditPaymentTargets) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.security_filters,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Security),
|
||||
keywordsRes = R.string.security_filters_search_keywords,
|
||||
) { nav.nav(Route.SecurityFilters) },
|
||||
SettingsEntry(R.string.call_settings, SettingsIcon.Symbol(MaterialSymbols.Phone)) { nav.nav(Route.CallSettings) },
|
||||
SettingsEntry(R.string.translations, SettingsIcon.Symbol(MaterialSymbols.Translate)) { nav.nav(Route.UserSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.call_settings,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Phone),
|
||||
keywordsRes = R.string.call_settings_search_keywords,
|
||||
) { nav.nav(Route.CallSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.translations,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Translate),
|
||||
keywordsRes = R.string.translations_search_keywords,
|
||||
) { nav.nav(Route.UserSettings) },
|
||||
),
|
||||
)
|
||||
|
||||
@@ -81,8 +125,16 @@ fun buildSettingsCatalog(
|
||||
icon = SettingsIcon.Painter(R.drawable.ic_tor, 1),
|
||||
keywordsRes = R.string.privacy_options_search_keywords,
|
||||
) { nav.nav(Route.PrivacyOptions) },
|
||||
SettingsEntry(R.string.ots_explorer_settings, SettingsIcon.Symbol(MaterialSymbols.Search)) { nav.nav(Route.OtsSettings) },
|
||||
SettingsEntry(R.string.namecoin_settings, SettingsIcon.Symbol(MaterialSymbols.Security)) { nav.nav(Route.NamecoinSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.ots_explorer_settings,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Search),
|
||||
keywordsRes = R.string.ots_explorer_search_keywords,
|
||||
) { nav.nav(Route.OtsSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.namecoin_settings,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Security),
|
||||
keywordsRes = R.string.namecoin_search_keywords,
|
||||
) { nav.nav(Route.NamecoinSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.ui_preferences,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Settings),
|
||||
@@ -93,12 +145,36 @@ fun buildSettingsCatalog(
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Notifications),
|
||||
keywordsRes = R.string.notification_settings_search_keywords,
|
||||
) { nav.nav(Route.NotificationSettings) },
|
||||
SettingsEntry(R.string.calendar_reminder_settings_title, SettingsIcon.Symbol(MaterialSymbols.CalendarMonth)) { nav.nav(Route.CalendarReminderSettings) },
|
||||
SettingsEntry(R.string.compose_settings, SettingsIcon.Symbol(MaterialSymbols.Edit)) { nav.nav(Route.ComposeSettings) },
|
||||
SettingsEntry(R.string.reactions_settings, SettingsIcon.Symbol(MaterialSymbols.ThumbUp)) { nav.nav(Route.ReactionsSettings) },
|
||||
SettingsEntry(R.string.bottom_bar_settings, SettingsIcon.Symbol(MaterialSymbols.Dashboard)) { nav.nav(Route.BottomBarSettings) },
|
||||
SettingsEntry(R.string.home_tabs_settings, SettingsIcon.Symbol(MaterialSymbols.Home)) { nav.nav(Route.HomeTabsSettings) },
|
||||
SettingsEntry(R.string.profile_ui_settings, SettingsIcon.Symbol(MaterialSymbols.AccountCircle)) { nav.nav(Route.ProfileUiSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.calendar_reminder_settings_title,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.CalendarMonth),
|
||||
keywordsRes = R.string.calendar_reminder_search_keywords,
|
||||
) { nav.nav(Route.CalendarReminderSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.compose_settings,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Edit),
|
||||
keywordsRes = R.string.compose_search_keywords,
|
||||
) { nav.nav(Route.ComposeSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.reactions_settings,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.ThumbUp),
|
||||
keywordsRes = R.string.reactions_settings_search_keywords,
|
||||
) { nav.nav(Route.ReactionsSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.bottom_bar_settings,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Dashboard),
|
||||
keywordsRes = R.string.bottom_bar_search_keywords,
|
||||
) { nav.nav(Route.BottomBarSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.home_tabs_settings,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Home),
|
||||
keywordsRes = R.string.home_tabs_search_keywords,
|
||||
) { nav.nav(Route.HomeTabsSettings) },
|
||||
SettingsEntry(
|
||||
titleRes = R.string.profile_ui_settings,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.AccountCircle),
|
||||
keywordsRes = R.string.profile_ui_search_keywords,
|
||||
) { nav.nav(Route.ProfileUiSettings) },
|
||||
),
|
||||
)
|
||||
|
||||
@@ -109,11 +185,40 @@ fun buildSettingsCatalog(
|
||||
entries =
|
||||
buildList {
|
||||
if (hasPrivateKey) {
|
||||
add(SettingsEntry(R.string.backup_keys, SettingsIcon.Symbol(MaterialSymbols.Key), isDanger = true) { nav.nav(Route.AccountBackup) })
|
||||
add(SettingsEntry(R.string.request_to_vanish, SettingsIcon.Symbol(MaterialSymbols.DeleteForever), isDanger = true) { nav.nav(Route.RequestToVanish) })
|
||||
add(
|
||||
SettingsEntry(
|
||||
titleRes = R.string.backup_keys,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.Key),
|
||||
keywordsRes = R.string.backup_keys_search_keywords,
|
||||
isDanger = true,
|
||||
) { nav.nav(Route.AccountBackup) },
|
||||
)
|
||||
add(
|
||||
SettingsEntry(
|
||||
titleRes = R.string.request_to_vanish,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.DeleteForever),
|
||||
keywordsRes = R.string.request_to_vanish_search_keywords,
|
||||
isDanger = true,
|
||||
) { nav.nav(Route.RequestToVanish) },
|
||||
)
|
||||
}
|
||||
add(SettingsEntry(R.string.vanish_history, SettingsIcon.Symbol(MaterialSymbols.History), isDanger = true) { nav.nav(Route.VanishEvents) })
|
||||
add(SettingsEntry(R.string.reset_marmot_state, SettingsIcon.Symbol(MaterialSymbols.DeleteSweep), isDanger = true, onClick = onResetMarmot))
|
||||
add(
|
||||
SettingsEntry(
|
||||
titleRes = R.string.vanish_history,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.History),
|
||||
keywordsRes = R.string.vanish_history_search_keywords,
|
||||
isDanger = true,
|
||||
) { nav.nav(Route.VanishEvents) },
|
||||
)
|
||||
add(
|
||||
SettingsEntry(
|
||||
titleRes = R.string.reset_marmot_state,
|
||||
icon = SettingsIcon.Symbol(MaterialSymbols.DeleteSweep),
|
||||
keywordsRes = R.string.reset_marmot_search_keywords,
|
||||
isDanger = true,
|
||||
onClick = onResetMarmot,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1516,6 +1516,29 @@
|
||||
<string name="notification_settings_search_keywords">push, alerts, sounds, vibration</string>
|
||||
<string name="security_filters_search_keywords">spam, block, mute, filter, warnings</string>
|
||||
<string name="zaps_search_keywords">lightning, sats, tips, wallet, amount</string>
|
||||
<string name="media_servers_search_keywords">blossom, uploads, images, photos, files, cdn, storage</string>
|
||||
<string name="event_sync_search_keywords">negentropy, sync, reconcile, backfill</string>
|
||||
<string name="import_follows_search_keywords">contacts, follows, follow list, import</string>
|
||||
<string name="nests_servers_search_keywords">audio rooms, live, spaces, rooms</string>
|
||||
<string name="profile_badges_search_keywords">badges, awards</string>
|
||||
<string name="favorite_dvms_search_keywords">dvm, data vending machine, algo, algorithm, feeds</string>
|
||||
<string name="reactions_search_keywords">emoji, like, reaction</string>
|
||||
<string name="video_player_search_keywords">video, player, playback, autoplay, mute</string>
|
||||
<string name="payment_targets_search_keywords">zap split, split, recipients, forward zaps</string>
|
||||
<string name="call_settings_search_keywords">webrtc, video call, voice call, calls</string>
|
||||
<string name="translations_search_keywords">language, translate, locale</string>
|
||||
<string name="ots_explorer_search_keywords">opentimestamps, timestamp, ots, proof</string>
|
||||
<string name="namecoin_search_keywords">namecoin, dns, identity, name</string>
|
||||
<string name="calendar_reminder_search_keywords">calendar, events, reminders, rsvp</string>
|
||||
<string name="compose_search_keywords">draft, posting, editor, auto-save</string>
|
||||
<string name="reactions_settings_search_keywords">emoji, reactions, like</string>
|
||||
<string name="bottom_bar_search_keywords">navigation, tabs, nav bar</string>
|
||||
<string name="home_tabs_search_keywords">tabs, feeds, threads, conversations</string>
|
||||
<string name="profile_ui_search_keywords">profile, layout</string>
|
||||
<string name="backup_keys_search_keywords">nsec, private key, seed, mnemonic, export</string>
|
||||
<string name="request_to_vanish_search_keywords">delete account, vanish, gdpr</string>
|
||||
<string name="vanish_history_search_keywords">deletion, delete events</string>
|
||||
<string name="reset_marmot_search_keywords">mls, group chat, messaging, reset</string>
|
||||
|
||||
<string name="danger_zone">Danger Zone</string>
|
||||
<string name="reset_marmot_state">Reset Marmot State</string>
|
||||
|
||||
+19
@@ -126,4 +126,23 @@ class SettingsCatalogFilterTest {
|
||||
assertTrue(result[0].isDanger)
|
||||
assertTrue(result[0].entries[0].isDanger)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun prefixOfAWordMatches() {
|
||||
// "rel" is a prefix of "Relay" (title); "the" is a prefix of "theme" (keyword).
|
||||
assertEquals(1, run("rel")[0].entries[0].titleRes)
|
||||
assertEquals(2, run("the")[0].entries[0].titleRes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun midWordTermDoesNotMatch() {
|
||||
// Word-prefix, not substring: "ackup" is inside "Backup" but not a prefix of any word.
|
||||
assertTrue(run("ackup").isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun everyQueryTermMustPrefixSomeWord() {
|
||||
assertEquals(2, run("dark size")[0].entries[0].titleRes) // both terms hit UI Preferences
|
||||
assertTrue(run("dark zzz").isEmpty()) // second term matches nothing
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user