Improves the order of search results

This commit is contained in:
Vitor Pamplona
2026-03-09 11:25:00 -04:00
parent 76598c304e
commit 2387a1bb91
7 changed files with 105 additions and 59 deletions
@@ -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 },
),
@@ -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
@@ -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<DualCase>) = metadataOrNull()?.containsAny(hiddenWordsCase) == true
}
fun Set<User>.toHexSet() = mapTo(LinkedHashSet(size)) { it.pubkeyHex }
@@ -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<DualCase>): Boolean = flow.value?.info?.anyNameOrAddressContains(terms) ?: false
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
fun anyNameStartsWith(terms: List<DualCase>): Boolean = flow.value?.info?.anyNameStartsWith(terms) ?: false
fun anyAddressStartsWith(terms: List<DualCase>): Boolean = flow.value?.info?.anyAddressStartsWith(terms) ?: false
fun anyPropertyContains(hiddenWordsCase: List<DualCase>): 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()
@@ -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 },
),
@@ -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<DualCase>): 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<DualCase>): Boolean =
name?.startsWithAny(terms) == true ||
displayName?.startsWithAny(terms) == true
fun anyAddressStartsWith(terms: List<DualCase>): Boolean =
lud16?.startsWithAny(terms) == true ||
nip05?.startsWithAny(terms) == true ||
website?.startsWithAny(terms) == true
fun anyPropertyContains(terms: List<DualCase>): 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 {
@@ -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<DualCase>): Boolean {
if (terms.isEmpty()) return true // Empty string is contained
@@ -78,6 +99,16 @@ fun String.containsAny(terms: List<DualCase>): Boolean {
return terms.any { containsIgnoreCase(it.lowercase, it.uppercase) }
}
fun String.startsWithAny(terms: List<DualCase>): 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,