damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }