From 7f39a18a0212682eabfea3433f8d25c81fe1836c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 15:07:13 +0000 Subject: [PATCH 1/2] fix: exclude client tag from note text search matching Searching for an app name (e.g. "Amethyst") was returning every event published through that client, because the local-cache note search matched the search term against all tag values including the NIP-89 ["client", ...] tag. Skip the client tag when matching tag values in findNotesStartingWith. https://claude.ai/code/session_01YMs6aXuvs5NaYjzyPH6Zqj --- .../amethyst/model/LocalCache.kt | 12 ++++- .../quartz/nip01Core/core/TagArray.kt | 6 ++- .../quartz/nip01Core/core/TagArrayTest.kt | 50 +++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayTest.kt 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))) + } +} From 5263e3b1d9ec1cee5515233434673ccc1d4871a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 15:37:16 +0000 Subject: [PATCH 2/2] fix: exclude p, e, a, and alt tags from note text search matching Their values are ids or descriptions of other events, not content of the event itself, so they shouldn't make an event match a text search. https://claude.ai/code/session_01YMs6aXuvs5NaYjzyPH6Zqj --- .../vitorpamplona/amethyst/model/LocalCache.kt | 18 ++++++++++++++---- .../quartz/nip01Core/core/TagArrayTest.kt | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) 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 2423a0ea28..d880d85c74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -106,6 +106,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent @@ -147,6 +148,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip32Labeling.LabelEvent import com.vitorpamplona.quartz.nip34Git.grasp.UserGraspListEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent @@ -2382,11 +2384,19 @@ object LocalCache : ILocalCache, ICacheProvider { ) /** - * 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. + * Tag names whose values should not match text searches: the `client` tag + * names the app that published the event (searching for "Amethyst" would + * otherwise return every event posted through Amethyst), and `p`/`e`/`a`/`alt` + * values are ids or descriptions of other events, not content of this one. */ - private val excludedTagNamesFromSearch = setOf(ClientTag.TAG_NAME) + private val excludedTagNamesFromSearch = + setOf( + ClientTag.TAG_NAME, + PTag.TAG_NAME, + ETag.TAG_NAME, + ATag.TAG_NAME, + AltTag.TAG_NAME, + ) fun findNotesStartingWith( text: String, 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 index 278f08c703..f610716e8b 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayTest.kt @@ -20,6 +20,10 @@ */ package com.vitorpamplona.quartz.nip01Core.core +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.ClientTag import kotlin.test.Test import kotlin.test.assertFalse @@ -47,4 +51,15 @@ class TagArrayTest { // other tags still match when the client tag is excluded assertTrue(tags.tagValueContains("recommendations", ignoreCase = true, exceptNames = setOf(ClientTag.TAG_NAME))) } + + @Test + fun skipsAllSearchExcludedTagNames() { + val excluded = setOf(ClientTag.TAG_NAME, PTag.TAG_NAME, ETag.TAG_NAME, ATag.TAG_NAME, AltTag.TAG_NAME) + + assertFalse(tags.tagValueContains("amethyst", ignoreCase = true, exceptNames = excluded)) + assertFalse(tags.tagValueContains("31990:460c25e6", ignoreCase = true, exceptNames = excluded)) + assertFalse(tags.tagValueContains("recommendations", ignoreCase = true, exceptNames = excluded)) + // non-excluded tags still match + assertTrue(tags.tagValueContains("30022", ignoreCase = true, exceptNames = excluded)) + } }