From bc9adc8352d8dbaeeaa3146dee3e48ba28c9187e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 9 Feb 2026 16:12:11 -0500 Subject: [PATCH] Breaks the search filter down into two subscriptions to prioritize Metadata without punishing content. --- .../searchCommand/SearchFilterAssembler.kt | 6 +- .../SearchPostWatcherSubAssembler.kt | 96 +++++++++++++++++++ ...er.kt => SearchUserWatcherSubAssembler.kt} | 15 ++- 3 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostWatcherSubAssembler.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/{SearchWatcherSubAssembler.kt => SearchUserWatcherSubAssembler.kt} (89%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt index 2bf9ae497..3e13f429f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt @@ -25,7 +25,8 @@ import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManager import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.MutableQueryState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies.SearchWatcherSubAssembler +import com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies.SearchPostWatcherSubAssembler +import com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies.SearchUserWatcherSubAssembler import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -46,7 +47,8 @@ class SearchFilterAssembler( ) : MutableComposeSubscriptionManager(scope) { val group = listOf( - SearchWatcherSubAssembler(cache, client, ::allKeys), + SearchUserWatcherSubAssembler(cache, client, ::allKeys), + SearchPostWatcherSubAssembler(cache, client, ::allKeys), ) override fun invalidateFilters() = group.forEach { it.invalidateFilters() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostWatcherSubAssembler.kt new file mode 100644 index 000000000..94b63bcad --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostWatcherSubAssembler.kt @@ -0,0 +1,96 @@ +/* + * 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.service.relayClient.searchCommand.subassemblies + +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed +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.NRelay +import com.vitorpamplona.quartz.nip19Bech32.entities.NSec +import com.vitorpamplona.quartz.utils.Hex + +/** + * Creates a new sub for each Search Screen. The StateFlow of the text field + * that has the search ter is the id of the sub. + */ +class SearchPostWatcherSubAssembler( + val cache: LocalCache, + client: INostrClient, + allKeys: () -> Set, +) : PerUniqueIdEoseManager(client, allKeys) { + override fun updateFilter( + key: SearchQueryState, + since: SincePerRelayMap?, + ): List? { + val mySearchString = key.searchQuery.value + + if (mySearchString.isBlank()) return null + + val defaultRelaysWithSearch = key.account.followPlusAllMineWithSearch.flow.value + + val directFilters = + runCatching { + if (Hex.isHex(mySearchString)) { + val hexKey = Hex.decode(mySearchString).toHexKey() + filterByEvent(hexKey, defaultRelaysWithSearch) + } else { + val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity + if (parsed != null) { + cache.consume(parsed) + + when (parsed) { + is NSec -> emptyList() + is NPub -> emptyList() + is NProfile -> emptyList() + is NNote -> filterByEvent(parsed.hex, defaultRelaysWithSearch) + is NEvent -> filterByEvent(parsed.hex, defaultRelaysWithSearch) + is NEmbed -> emptyList() + is NRelay -> emptyList() + is NAddress -> filterByAddress(parsed, defaultRelaysWithSearch) + else -> emptyList() + } + } else { + emptyList() + } + } + }.getOrDefault(emptyList()) + + val searchFilters = + key.account.searchRelayList.flow.value.flatMap { + searchPostsByText(mySearchString, it) + } + + return directFilters + searchFilters + } + + override fun id(key: SearchQueryState) = key.searchQuery.hashCode() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchUserWatcherSubAssembler.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchUserWatcherSubAssembler.kt index 79fd7c3f7..1fa7801e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchUserWatcherSubAssembler.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.quartz.utils.Hex * Creates a new sub for each Search Screen. The StateFlow of the text field * that has the search ter is the id of the sub. */ -class SearchWatcherSubAssembler( +class SearchUserWatcherSubAssembler( val cache: LocalCache, client: INostrClient, allKeys: () -> Set, @@ -65,7 +65,7 @@ class SearchWatcherSubAssembler( runCatching { if (Hex.isHex(mySearchString)) { val hexKey = Hex.decode(mySearchString).toHexKey() - filterByAuthor(hexKey, indexRelays, defaultRelaysWithSearch) + filterByEvent(hexKey, defaultRelaysWithSearch) + filterByAuthor(hexKey, indexRelays, defaultRelaysWithSearch) } else { val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity if (parsed != null) { @@ -75,11 +75,11 @@ class SearchWatcherSubAssembler( is NSec -> filterByAuthor(parsed.toPubKeyHex(), indexRelays, defaultRelaysWithSearch) is NPub -> filterByAuthor(parsed.hex, indexRelays, defaultRelaysWithSearch) is NProfile -> filterByAuthor(parsed.hex, indexRelays, defaultRelaysWithSearch) - is NNote -> filterByEvent(parsed.hex, defaultRelaysWithSearch) - is NEvent -> filterByEvent(parsed.hex, defaultRelaysWithSearch) + is NNote -> emptyList() + is NEvent -> emptyList() is NEmbed -> emptyList() is NRelay -> emptyList() - is NAddress -> filterByAddress(parsed, defaultRelaysWithSearch) + is NAddress -> emptyList() else -> emptyList() } } else { @@ -91,10 +91,7 @@ class SearchWatcherSubAssembler( val searchFilters = key.account.searchRelayList.flow.value.flatMap { searchPeopleByName(mySearchString, it) - } + - key.account.searchRelayList.flow.value.flatMap { - searchPostsByText(mySearchString, it) - } + } return directFilters + searchFilters }