From 2387a1bb915c873b44f17fd1bfccec9cfc87b7d2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 9 Mar 2026 11:25:00 -0400 Subject: [PATCH] Improves the order of search results --- .../amethyst/model/LocalCache.kt | 25 +++++++--- .../amethyst/commons/model/Note.kt | 2 +- .../amethyst/commons/model/User.kt | 3 -- .../model/nip01Core/UserMetadataCache.kt | 47 +++---------------- .../desktop/cache/DesktopLocalCache.kt | 21 +++++++-- .../quartz/nip01Core/metadata/UserMetadata.kt | 35 ++++++++++++-- .../vitorpamplona/quartz/utils/StringUtils.kt | 31 ++++++++++++ 7 files changed, 105 insertions(+), 59 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 7e8a21219..c02b95a7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -204,6 +204,7 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent import com.vitorpamplona.quartz.nipC0CodeSnippets.CodeSnippetEvent +import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils @@ -2276,20 +2277,32 @@ object LocalCache : ILocalCache, ICacheProvider { } } + val dualCase = + listOf( + DualCase(username.lowercase(), username.uppercase()), + ) + val finds = users.filter { _, user: User -> - ( - (user.metadataOrNull()?.anyNameStartsWith(username) == true) || - user.pubkeyHex.startsWith(username, true) || + val metadata = user.metadataOrNull() + if (metadata == null) { + user.pubkeyHex.startsWith(username, true) || user.pubkeyNpub().startsWith(username, true) - ) && - (forAccount == null || (!forAccount.isHidden(user) && !user.containsAny(forAccount.hiddenUsers.flow.value.hiddenWordsCase))) + } else { + ( + metadata.anyNameOrAddressContains(dualCase) || + user.pubkeyHex.startsWith(username, true) || + user.pubkeyNpub().startsWith(username, true) + ) && + (forAccount == null || (!forAccount.isHidden(user) && !metadata.anyPropertyContains(forAccount.hiddenUsers.flow.value.hiddenWordsCase))) + } } return finds.sortedWith( compareBy( { forAccount?.isFollowing(it) == false }, - { !it.toBestDisplayName().startsWith(username, ignoreCase = true) }, + { it.metadataOrNull()?.anyNameStartsWith(dualCase) == false }, + { it.metadataOrNull()?.anyAddressStartsWith(dualCase) == false }, { it.toBestDisplayName().lowercase() }, { it.pubkeyHex }, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt index 9e95521a2..a66d32bab 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt @@ -890,7 +890,7 @@ open class Note( return true } - if (author?.containsAny(accountChoices.hiddenWordsCase) == true) return true + if (author?.metadataOrNull()?.anyPropertyContains(accountChoices.hiddenWordsCase) == true) return true } return false diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt index ed4fbc59f..10f727abc 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt @@ -36,7 +36,6 @@ import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.utils.Hex interface UserDependencies @@ -141,8 +140,6 @@ class User( fun relayStateOrNull(): UserRelaysCache? = relays fun relayState(): UserRelaysCache = relays ?: UserRelaysCache().also { relays = it } - - fun containsAny(hiddenWordsCase: List) = metadataOrNull()?.containsAny(hiddenWordsCase) == true } fun Set.toHexSet() = mapTo(LinkedHashSet(size)) { it.pubkeyHex } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserMetadataCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserMetadataCache.kt index 99b11e05d..ffa1e2537 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserMetadataCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserMetadataCache.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaimTag import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims import com.vitorpamplona.quartz.utils.DualCase -import com.vitorpamplona.quartz.utils.containsAny import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -60,48 +59,16 @@ class UserMetadataCache { fun shouldUpdateWith(event: MetadataEvent) = event.createdAt > (flow.value?.createdAt ?: 0) - fun anyNameStartsWith(username: String): Boolean = flow.value?.info?.anyNameStartsWith(username) ?: false + fun anyNameOrAddressContains(terms: List): Boolean = flow.value?.info?.anyNameOrAddressContains(terms) ?: false - fun containsAny(hiddenWordsCase: List): Boolean { + fun anyNameStartsWith(terms: List): Boolean = flow.value?.info?.anyNameStartsWith(terms) ?: false + + fun anyAddressStartsWith(terms: List): Boolean = flow.value?.info?.anyAddressStartsWith(terms) ?: false + + fun anyPropertyContains(hiddenWordsCase: List): Boolean { if (hiddenWordsCase.isEmpty()) return false - flow.value?.let { userInfo -> - val info = userInfo.info - - if (info.name?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info.displayName?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info.picture?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info.banner?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info.about?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info.lud06?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info.lud16?.containsAny(hiddenWordsCase) == true) { - return true - } - - if (info.nip05?.containsAny(hiddenWordsCase) == true) { - return true - } - } - - return false + return flow.value?.info?.anyPropertyContains(hiddenWordsCase) ?: false } fun bestName(): String? = flow.value?.info?.bestName() diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt index ba278453f..c51526c62 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.utils.DualCase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -85,15 +86,27 @@ class DesktopLocalCache : ICacheProvider { if (user != null) return listOf(user) } + val dualCase = + listOf( + DualCase(prefix.lowercase(), prefix.uppercase()), + ) + // Search by name/displayName/nip05/lud16 return users.values .filter { user -> - user.metadataOrNull()?.anyNameStartsWith(prefix) == true || - user.pubkeyHex.startsWith(prefix, ignoreCase = true) || - user.pubkeyNpub().startsWith(prefix, ignoreCase = true) + val metadata = user.metadataOrNull() + if (metadata == null) { + user.pubkeyHex.startsWith(prefix, true) || + user.pubkeyNpub().startsWith(prefix, true) + } else { + metadata.anyNameOrAddressContains(dualCase) || + user.pubkeyHex.startsWith(prefix, true) || + user.pubkeyNpub().startsWith(prefix, true) + } }.sortedWith( compareBy( - { !it.toBestDisplayName().startsWith(prefix, ignoreCase = true) }, + { it.metadataOrNull()?.anyNameStartsWith(dualCase) == false }, + { it.metadataOrNull()?.anyAddressStartsWith(dualCase) == false }, { it.toBestDisplayName().lowercase() }, { it.pubkeyHex }, ), diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt index 65eb02df7..639e3fb45 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt @@ -22,6 +22,9 @@ package com.vitorpamplona.quartz.nip01Core.metadata import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.lightning.Lud06 +import com.vitorpamplona.quartz.utils.DualCase +import com.vitorpamplona.quartz.utils.containsAny +import com.vitorpamplona.quartz.utils.startsWithAny import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -47,11 +50,6 @@ class UserMetadata { fun anyName(): String? = displayName ?: name - fun anyNameStartsWith(prefix: String): Boolean = - listOfNotNull(name, displayName, nip05, lud06, lud16).any { - it.contains(prefix, true) - } - fun lnAddress(): String? = lud16 ?: lud06 fun bestName(): String? = displayName ?: name @@ -104,6 +102,33 @@ class UserMetadata { if (pronouns?.isBlank() == true) pronouns = null } + fun anyNameOrAddressContains(terms: List): Boolean = + name?.containsAny(terms) == true || + displayName?.containsAny(terms) == true || + lud16?.containsAny(terms) == true || + nip05?.containsAny(terms) == true || + website?.containsAny(terms) == true + + fun anyNameStartsWith(terms: List): Boolean = + name?.startsWithAny(terms) == true || + displayName?.startsWithAny(terms) == true + + fun anyAddressStartsWith(terms: List): Boolean = + lud16?.startsWithAny(terms) == true || + nip05?.startsWithAny(terms) == true || + website?.startsWithAny(terms) == true + + fun anyPropertyContains(terms: List): Boolean = + name?.containsAny(terms) == true || + displayName?.containsAny(terms) == true || + picture?.containsAny(terms) == true || + banner?.containsAny(terms) == true || + about?.containsAny(terms) == true || + lud06?.containsAny(terms) == true || + lud16?.containsAny(terms) == true || + nip05?.containsAny(terms) == true || + website?.containsAny(terms) == true + fun convertLud06toLud16IfNeeded() { if (lud16.isNullOrBlank()) { lud06?.let { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/StringUtils.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/StringUtils.kt index ef76c2119..8fbb3e046 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/StringUtils.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/StringUtils.kt @@ -68,6 +68,27 @@ fun String.containsIgnoreCase( return false } +fun String.startsWithIgnoreCase( + whatLowercase: String, + whatUppercase: String, +): Boolean { + val termLength = min(whatUppercase.length, whatLowercase.length) + + // If the search term is longer than the string itself, it can't be a prefix + if (this.length < termLength) return false + + var offset = 0 + while (offset < termLength) { + val current = this[offset] + if (current != whatLowercase[offset] && current != whatUppercase[offset]) { + return false + } + offset++ + } + + return true +} + fun String.containsAny(terms: List): Boolean { if (terms.isEmpty()) return true // Empty string is contained @@ -78,6 +99,16 @@ fun String.containsAny(terms: List): Boolean { return terms.any { containsIgnoreCase(it.lowercase, it.uppercase) } } +fun String.startsWithAny(terms: List): Boolean { + if (terms.isEmpty()) return true // Empty string is contained + + if (terms.size == 1) { + return startsWithIgnoreCase(terms[0].lowercase, terms[0].uppercase) + } + + return terms.any { startsWithIgnoreCase(it.lowercase, it.uppercase) } +} + class DualCase( val lowercase: String, val uppercase: String,