UserSearchCache.swift (4516B)
1 // 2 // UserSearchCache.swift 3 // damus 4 // 5 // Created by Terry Yiu on 6/27/23. 6 // 7 8 import Foundation 9 10 /// Cache of searchable users by name, display_name, NIP-05 identifier, or own contact list petname. 11 /// Optimized for fast searches of substrings by using a Trie. 12 /// Optimal for performing user searches that could be initiated by typing quickly on a keyboard into a text input field. 13 14 // TODO: replace with lmdb (the b tree should handle this just fine ?) 15 // we just need a name to profile index 16 class UserSearchCache { 17 private let trie = Trie<Pubkey>() 18 19 func search(key: String) -> [Pubkey] { 20 let results = trie.find(key: key) 21 return results 22 } 23 24 /// Computes the differences between an old profile, if it exists, and a new profile, and updates the user search cache accordingly. 25 @MainActor 26 func updateProfile(id: Pubkey, profiles: Profiles, oldProfile: Profile?, newProfile: Profile) { 27 // Remove searchable keys tied to the old profile if they differ from the new profile 28 // to keep the trie clean without empty nodes while avoiding excessive graph searching. 29 if let oldProfile { 30 if let oldName = oldProfile.name, newProfile.name?.caseInsensitiveCompare(oldName) != .orderedSame { 31 trie.remove(key: oldName.lowercased(), value: id) 32 } 33 if let oldDisplayName = oldProfile.display_name, newProfile.display_name?.caseInsensitiveCompare(oldDisplayName) != .orderedSame { 34 trie.remove(key: oldDisplayName.lowercased(), value: id) 35 } 36 if let oldNip05 = oldProfile.nip05, newProfile.nip05?.caseInsensitiveCompare(oldNip05) != .orderedSame { 37 trie.remove(key: oldNip05.lowercased(), value: id) 38 } 39 } 40 41 addProfile(id: id, profiles: profiles, profile: newProfile) 42 } 43 44 /// Adds a profile to the user search cache. 45 @MainActor 46 private func addProfile(id: Pubkey, profiles: Profiles, profile: Profile) { 47 // Searchable by name. 48 if let name = profile.name { 49 trie.insert(key: name.lowercased(), value: id) 50 } 51 52 // Searchable by display name. 53 if let displayName = profile.display_name { 54 trie.insert(key: displayName.lowercased(), value: id) 55 } 56 57 // Searchable by NIP-05 identifier. 58 if let nip05 = profiles.is_validated(id) { 59 trie.insert(key: "\(nip05.username.lowercased())@\(nip05.host.lowercased())", value: id) 60 } 61 } 62 63 /// Computes the diffences between an old contacts event and a new contacts event for our own user, and updates the search cache accordingly. 64 func updateOwnContactsPetnames(id: Pubkey, oldEvent: NostrEvent?, newEvent: NostrEvent) { 65 guard newEvent.known_kind == .contacts && newEvent.pubkey == id else { 66 return 67 } 68 69 var petnames: [Pubkey: String] = [:] 70 for tag in newEvent.tags { 71 guard tag.count > 3, 72 let chr = tag[0].single_char, chr == "p", 73 let id = tag[1].id() 74 else { 75 return 76 } 77 78 let pubkey = Pubkey(id) 79 80 petnames[pubkey] = tag[3].string() 81 } 82 83 // Compute the diff with the old contacts list, if it exists, 84 // mark the ones that are the same to not be removed from the user search cache, 85 // and remove the old ones that are different from the user search cache. 86 if let oldEvent, oldEvent.known_kind == .contacts, oldEvent.pubkey == id { 87 for tag in oldEvent.tags { 88 guard tag.count >= 4, 89 tag[0].matches_char("p"), 90 let id = tag[1].id() 91 else { 92 return 93 } 94 95 let pubkey = Pubkey(id) 96 97 let oldPetname = tag[3].string() 98 99 if let newPetname = petnames[pubkey] { 100 if newPetname.caseInsensitiveCompare(oldPetname) == .orderedSame { 101 petnames.removeValue(forKey: pubkey) 102 } else { 103 trie.remove(key: oldPetname, value: pubkey) 104 } 105 } else { 106 trie.remove(key: oldPetname, value: pubkey) 107 } 108 } 109 } 110 111 // Add the new petnames to the user search cache. 112 for (pubkey, petname) in petnames { 113 trie.insert(key: petname, value: pubkey) 114 } 115 } 116 }