diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index b9d7d39e4f..2423a0ea28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -252,6 +252,7 @@ import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent +import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.ClientTag import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent @@ -2380,6 +2381,13 @@ object LocalCache : ILocalCache, ICacheProvider { note.event is AppSpecificDataEvent ) + /** + * Tag names whose values should not match text searches. The `client` tag + * names the app that published the event, so searching for "Amethyst" + * would otherwise return every event posted through Amethyst. + */ + private val excludedTagNamesFromSearch = setOf(ClientTag.TAG_NAME) + fun findNotesStartingWith( text: String, hiddenUsers: HiddenUsersState, @@ -2419,7 +2427,7 @@ object LocalCache : ILocalCache, ICacheProvider { return@filter false } - if (note.event?.tags?.tagValueContains(text, true) == true || + if (note.event?.tags?.tagValueContains(text, true, excludedTagNamesFromSearch) == true || note.idHex.startsWith(text, true) ) { return@filter !note.isHiddenFor(hiddenUsers.flow.value) @@ -2440,7 +2448,7 @@ object LocalCache : ILocalCache, ICacheProvider { return@filter false } - if (addressable.event?.tags?.tagValueContains(text, true) == true || + if (addressable.event?.tags?.tagValueContains(text, true, excludedTagNamesFromSearch) == true || addressable.idHex.startsWith(text, true) ) { return@filter !addressable.isHiddenFor(hiddenUsers.flow.value) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt index 40e191d70b..89e4accaec 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt @@ -171,12 +171,14 @@ fun TagArray.isAnyLowercaseTagged( ) = this.fastAny { it.size > 1 && it[0] == tagName && it[1].lowercase() in tagValues } /** - * Returns `true` if at least one tag has value that contains [text] + * Returns `true` if at least one tag has value that contains [text]. + * Tags whose name is in [exceptNames] are skipped. */ fun TagArray.tagValueContains( text: String, ignoreCase: Boolean = false, -) = this.fastAny { it.size > 1 && it[1].contains(text, ignoreCase) } + exceptNames: Set = emptySet(), +) = this.fastAny { it.size > 1 && it[0] !in exceptNames && it[1].contains(text, ignoreCase) } fun TagArray.containsAllTagNamesWithValues(names: Set): Boolean { val remaining = names.toMutableSet() diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayTest.kt new file mode 100644 index 0000000000..278f08c703 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayTest.kt @@ -0,0 +1,50 @@ +/* + * 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.quartz.nip01Core.core + +import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.ClientTag +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class TagArrayTest { + private val tags: TagArray = + arrayOf( + arrayOf("d", "30022"), + arrayOf("a", "31990:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:1685802317447", "wss://nostr.wine/", "android"), + arrayOf("alt", "App recommendations by the author"), + arrayOf("client", "Amethyst"), + ) + + @Test + fun matchesTagValues() { + assertTrue(tags.tagValueContains("recommendations", ignoreCase = true)) + assertTrue(tags.tagValueContains("amethyst", ignoreCase = true)) + assertFalse(tags.tagValueContains("missing", ignoreCase = true)) + } + + @Test + fun skipsExcludedTagNames() { + assertFalse(tags.tagValueContains("amethyst", ignoreCase = true, exceptNames = setOf(ClientTag.TAG_NAME))) + // other tags still match when the client tag is excluded + assertTrue(tags.tagValueContains("recommendations", ignoreCase = true, exceptNames = setOf(ClientTag.TAG_NAME))) + } +}