diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 09103f60..77bf2182 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -20,11 +20,7 @@ 3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; }; 3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; }; 3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; }; - 3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5E47C42A4A6CF400C0D090 /* Trie.swift */; }; - 3A5E47C72A4A76C800C0D090 /* TrieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5E47C62A4A76C800C0D090 /* TrieTests.swift */; }; 3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; }; - 3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */; }; - 3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */; }; 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; }; 3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; }; 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; }; @@ -753,8 +749,6 @@ 3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = ""; }; 3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = ""; }; 3A5CAE1F298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = ""; }; - 3A5E47C42A4A6CF400C0D090 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = ""; }; - 3A5E47C62A4A76C800C0D090 /* TrieTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrieTests.swift; sourceTree = ""; }; 3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; 3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; 3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -768,8 +762,6 @@ 3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtil.swift; sourceTree = ""; }; - 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSearchCache.swift; sourceTree = ""; }; - 3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearchCacheTests.swift; sourceTree = ""; }; 3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = ""; }; 3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = ""; }; 3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = ""; }; @@ -1648,8 +1640,6 @@ 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */, 4C7D09772A0B0CC900943473 /* WalletModel.swift */, 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */, - 3A5E47C42A4A6CF400C0D090 /* Trie.swift */, - 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */, D723C38D2AB8D83400065664 /* ContentFilters.swift */, D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */, D7CB5D3D2B116DAD00AD4105 /* NotificationsManager.swift */, @@ -2559,8 +2549,6 @@ 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */, 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */, 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */, - 3A5E47C62A4A76C800C0D090 /* TrieTests.swift */, - 3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */, 4C4F14A62A2A61A30045A0B9 /* NostrScriptTests.swift */, 4C19AE542A5D977400C90DB7 /* HashtagTests.swift */, 3AAC7A012A60FE72002B50DF /* LocalizationUtilTests.swift */, @@ -3184,7 +3172,6 @@ 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */, D74AAFCF2B155D8C006CF0F4 /* ZapDataModel.swift in Sources */, 4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */, - 3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */, D7100C562B76F8E600C59298 /* PurpleViewPrimitives.swift in Sources */, B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */, D7EDED2E2B128E8A0018B19C /* CollectionExtension.swift in Sources */, @@ -3404,7 +3391,6 @@ 3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */, 5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */, D2277EEA2A089BD5006C3807 /* Router.swift in Sources */, - 3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */, 4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */, 4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, @@ -3549,7 +3535,6 @@ files = ( 4C2D34412BDAF1B300F9FB44 /* NIP10Tests.swift in Sources */, 4C684A572A7FFAE6005E6031 /* UrlTests.swift in Sources */, - 3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */, 4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */, 4C19AE552A5D977400C90DB7 /* HashtagTests.swift in Sources */, D72927AD2BAB515C00F93E90 /* RelayURLTests.swift in Sources */, @@ -3579,7 +3564,6 @@ 75AD872B2AA23A460085EF2C /* Block+Tests.swift in Sources */, E0E024112B7C19C20075735D /* TranslationTests.swift in Sources */, F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */, - 3A5E47C72A4A76C800C0D090 /* TrieTests.swift in Sources */, B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */, 4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */, D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */, diff --git a/damus/Models/Trie.swift b/damus/Models/Trie.swift deleted file mode 100644 index 998dc06a..00000000 --- a/damus/Models/Trie.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// Trie.swift -// damus -// -// Created by Terry Yiu on 6/26/23. -// - -import Foundation - -/// Tree data structure of all the substring permutations of a collection of strings optimized for searching for values of type V. -/// -/// Each node in the tree can have child nodes. -/// Each node represents a single character in substrings, and each of its child nodes represent the subsequent character in those substrings. -/// -/// A node that has no children mean that there are no substrings with any additional characters beyond the branch of letters leading up to that node. -/// -/// A node that has values mean that there are strings that end in the character represented by the node and contain the substring represented by the branch of letters leading up to that node. -/// -/// https://en.wikipedia.org/wiki/Trie -class Trie { - private var children: [Character : Trie] = [:] - - /// Separate exact matches from strict substrings so that exact matches appear first in returned results. - private var exactMatchValues = Set() - private var substringMatchValues = Set() - - private var parent: Trie? = nil -} - -extension Trie { - var hasChildren: Bool { - return !self.children.isEmpty - } - - var hasValues: Bool { - return !self.exactMatchValues.isEmpty || !self.substringMatchValues.isEmpty - } - - /// Finds the branch that matches the specified key and returns the values from all of its descendant nodes. - func find(key: String) -> [V] { - var currentNode = self - - // Find branch with matching prefix. - for char in key { - if let child = currentNode.children[char] { - currentNode = child - } else { - return [] - } - } - - // Perform breadth-first search from matching branch and collect values from all descendants. - var substringMatches = Set(currentNode.substringMatchValues) - var queue = Array(currentNode.children.values) - - while !queue.isEmpty { - let node = queue.removeFirst() - substringMatches.formUnion(node.exactMatchValues) - substringMatches.formUnion(node.substringMatchValues) - queue.append(contentsOf: node.children.values) - } - - // Prioritize exact matches to be returned first, and then remove exact matches from the set of partial substring matches that are appended afterward. - return Array(currentNode.exactMatchValues) + (substringMatches.subtracting(currentNode.exactMatchValues)) - } - - /// Inserts value of type V into this trie for the specified key. This function stores all substring endings of the key, not only the key itself. - /// Runtime performance is O(n^2) and storage cost is O(n), where n is the number of characters in the key. - func insert(key: String, value: V) { - // Create root branches for each character of the key to enable substring searches instead of only just prefix searches. - // Hence the nested loop. - for i in 0..() - - func search(key: String) -> [Pubkey] { - let results = trie.find(key: key) - return results - } - - /// Computes the differences between an old profile, if it exists, and a new profile, and updates the user search cache accordingly. - @MainActor - func updateProfile(id: Pubkey, profiles: Profiles, oldProfile: Profile?, newProfile: Profile) { - // Remove searchable keys tied to the old profile if they differ from the new profile - // to keep the trie clean without empty nodes while avoiding excessive graph searching. - if let oldProfile { - if let oldName = oldProfile.name, newProfile.name?.caseInsensitiveCompare(oldName) != .orderedSame { - trie.remove(key: oldName.lowercased(), value: id) - } - if let oldDisplayName = oldProfile.display_name, newProfile.display_name?.caseInsensitiveCompare(oldDisplayName) != .orderedSame { - trie.remove(key: oldDisplayName.lowercased(), value: id) - } - if let oldNip05 = oldProfile.nip05, newProfile.nip05?.caseInsensitiveCompare(oldNip05) != .orderedSame { - trie.remove(key: oldNip05.lowercased(), value: id) - } - } - - addProfile(id: id, profiles: profiles, profile: newProfile) - } - - /// Adds a profile to the user search cache. - @MainActor - private func addProfile(id: Pubkey, profiles: Profiles, profile: Profile) { - // Searchable by name. - if let name = profile.name { - trie.insert(key: name.lowercased(), value: id) - } - - // Searchable by display name. - if let displayName = profile.display_name { - trie.insert(key: displayName.lowercased(), value: id) - } - - // Searchable by NIP-05 identifier. - if let nip05 = profiles.is_validated(id) { - trie.insert(key: "\(nip05.username.lowercased())@\(nip05.host.lowercased())", value: id) - } - } - - /// Computes the diffences between an old contacts event and a new contacts event for our own user, and updates the search cache accordingly. - func updateOwnContactsPetnames(id: Pubkey, oldEvent: NostrEvent?, newEvent: NostrEvent) { - guard newEvent.known_kind == .contacts && newEvent.pubkey == id else { - return - } - - var petnames: [Pubkey: String] = [:] - for tag in newEvent.tags { - guard tag.count > 3, - let chr = tag[0].single_char, chr == "p", - let id = tag[1].id() - else { - return - } - - let pubkey = Pubkey(id) - - petnames[pubkey] = tag[3].string() - } - - // Compute the diff with the old contacts list, if it exists, - // mark the ones that are the same to not be removed from the user search cache, - // and remove the old ones that are different from the user search cache. - if let oldEvent, oldEvent.known_kind == .contacts, oldEvent.pubkey == id { - for tag in oldEvent.tags { - guard tag.count >= 4, - tag[0].matches_char("p"), - let id = tag[1].id() - else { - return - } - - let pubkey = Pubkey(id) - - let oldPetname = tag[3].string() - - if let newPetname = petnames[pubkey] { - if newPetname.caseInsensitiveCompare(oldPetname) == .orderedSame { - petnames.removeValue(forKey: pubkey) - } else { - trie.remove(key: oldPetname, value: pubkey) - } - } else { - trie.remove(key: oldPetname, value: pubkey) - } - } - } - - // Add the new petnames to the user search cache. - for (pubkey, petname) in petnames { - trie.insert(key: petname, value: pubkey) - } - } -} diff --git a/damusTests/TrieTests.swift b/damusTests/TrieTests.swift deleted file mode 100644 index e110f72e..00000000 --- a/damusTests/TrieTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// TrieTests.swift -// damusTests -// -// Created by Terry Yiu on 6/26/23. -// - -import XCTest -@testable import damus - -final class TrieTests: XCTestCase { - - func testFind() throws { - let trie = Trie() - - let keys = ["foobar", "food", "foo", "somethingelse", "duplicate", "duplicate"] - keys.forEach { - trie.insert(key: $0, value: $0) - } - - let allResults = trie.find(key: "") - XCTAssertEqual(Set(allResults), Set(["foobar", "food", "foo", "somethingelse", "duplicate"])) - - let fooResults = trie.find(key: "foo") - XCTAssertEqual(fooResults.first, "foo") - XCTAssertEqual(Set(fooResults), Set(["foobar", "food", "foo"])) - - let foodResults = trie.find(key: "food") - XCTAssertEqual(foodResults, ["food"]) - - let ooResults = trie.find(key: "oo") - XCTAssertEqual(Set(ooResults), Set(["foobar", "food", "foo"])) - - let aResults = trie.find(key: "a") - XCTAssertEqual(Set(aResults), Set(["foobar", "duplicate"])) - - let notFoundResults = trie.find(key: "notfound") - XCTAssertEqual(notFoundResults, []) - - // Sanity check that the root node has children. - XCTAssertTrue(trie.hasChildren) - - // Sanity check that the root node has no values. - XCTAssertFalse(trie.hasValues) - } - - func testRemove() { - let trie = Trie() - - let keys = ["foobar", "food", "foo", "somethingelse", "duplicate", "duplicate"] - keys.forEach { - trie.insert(key: $0, value: $0) - } - - keys.forEach { - trie.remove(key: $0, value: $0) - } - - let allResults = trie.find(key: "") - XCTAssertTrue(allResults.isEmpty) - - let fooResults = trie.find(key: "foo") - XCTAssertTrue(fooResults.isEmpty) - - let foodResults = trie.find(key: "food") - XCTAssertTrue(foodResults.isEmpty) - - let ooResults = trie.find(key: "oo") - XCTAssertTrue(ooResults.isEmpty) - - let aResults = trie.find(key: "a") - XCTAssertTrue(aResults.isEmpty) - - // Verify that removal of values from all the keys that were inserted in the trie previously also resulted in the cleanup of the trie. - XCTAssertFalse(trie.hasChildren) - XCTAssertFalse(trie.hasValues) - } - -} diff --git a/damusTests/UserSearchCacheTests.swift b/damusTests/UserSearchCacheTests.swift deleted file mode 100644 index 9c645a45..00000000 --- a/damusTests/UserSearchCacheTests.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// UserSearchCacheTests.swift -// damusTests -// -// Created by Terry Yiu on 6/30/23. -// - -import XCTest -@testable import damus - -// TODO: Update these tests to work with NostrDB Profile changes (https://github.com/damus-io/damus/issues/1586) -final class UserSearchCacheTests: XCTestCase { - /* - var keypair: FullKeypair? = nil - let damusState = test_damus_state - let nip05 = "_@somedomain.com" - - @MainActor - override func setUpWithError() throws { - keypair = try XCTUnwrap(generate_new_keypair()) - - if let keypair { - let pubkey = keypair.pubkey - let validatedNip05 = try XCTUnwrap(NIP05.parse(nip05)) - - damusState.profiles.set_validated(pubkey, nip05: validatedNip05) - - let profile = Profile(name: "tyiu", display_name: "Terry Yiu", about: nil, picture: nil, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nip05, damus_donation: nil) - - // Lookup to synchronize access on profiles dictionary to avoid race conditions. - let _ = damusState.profiles.lookup(id: pubkey) - } - } - - override func tearDown() { - keypair = nil - } - - func testSearch() throws { - let keypair = try XCTUnwrap(keypair) - XCTAssertEqual(damusState.user_search_cache.search(key: "tyiu"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "ty"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "terry yiu"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "rry"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "somedomain"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "dom"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "i"), [keypair.pubkey]) - } - - @MainActor - func testUpdateProfile() throws { - let keypair = try XCTUnwrap(keypair) - - let newNip05 = "_@other.xyz" - _ = try XCTUnwrap(NIP05.parse(newNip05)) - - damusState.profiles.set_validated(keypair.pubkey, nip05: NIP05.parse(newNip05)) - - // Old profile attributes are removed from cache. - XCTAssertEqual(damusState.user_search_cache.search(key: "tyiu"), []) - XCTAssertEqual(damusState.user_search_cache.search(key: "ty"), []) - XCTAssertEqual(damusState.user_search_cache.search(key: "Terry Yiu"), []) - XCTAssertEqual(damusState.user_search_cache.search(key: "rry"), []) - XCTAssertEqual(damusState.user_search_cache.search(key: "somedomain"), []) - XCTAssertEqual(damusState.user_search_cache.search(key: "dom"), []) - - // New profile attributes are added to cache. - XCTAssertEqual(damusState.user_search_cache.search(key: "whoami"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "hoa"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "t-dawg"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "daw"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "other"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "xyz"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "the"), [keypair.pubkey]) - XCTAssertEqual(damusState.user_search_cache.search(key: "y"), [keypair.pubkey]) - } - */ - - /* - func testUpdateOwnContactsPetnames() throws { - let keypair = try XCTUnwrap(keypair) - let damus = Pubkey(hex: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681")! - let jb55 = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")! - - var pubkeysToPetnames = [Pubkey: String]() - pubkeysToPetnames[damus] = "damus" - pubkeysToPetnames[jb55] = "jb55" - - let contactsEvent = try createContactsEventWithPetnames(pubkeysToPetnames: pubkeysToPetnames) - - // Initial own contacts event caching on searchable petnames. - damusState.user_search_cache.updateOwnContactsPetnames(id: keypair.pubkey, oldEvent: nil, newEvent: contactsEvent) - - XCTAssertEqual(damusState.user_search_cache.search(key: "damus"), [damus]) - XCTAssertEqual(damusState.user_search_cache.search(key: "jb55"), [jb55]) - XCTAssertEqual(damusState.user_search_cache.search(key: "5"), [jb55]) - - // Replace one of the petnames and verify if the cache updates accordingly. - - pubkeysToPetnames.removeValue(forKey: jb55) - pubkeysToPetnames[jb55] = "bill" - let newContactsEvent = try createContactsEventWithPetnames(pubkeysToPetnames: pubkeysToPetnames) - - damusState.user_search_cache.updateOwnContactsPetnames(id: keypair.pubkey, oldEvent: contactsEvent, newEvent: newContactsEvent) - - XCTAssertEqual(damusState.user_search_cache.search(key: "damus"), [damus]) - XCTAssertEqual(damusState.user_search_cache.search(key: "jb55"), []) - XCTAssertEqual(damusState.user_search_cache.search(key: "5"), []) - XCTAssertEqual(damusState.user_search_cache.search(key: "bill"), [jb55]) - XCTAssertEqual(damusState.user_search_cache.search(key: "l"), [jb55]) - } - */ - - /* - private func createContactsEventWithPetnames(pubkeysToPetnames: [Pubkey: String]) throws -> NostrEvent { - let keypair = try XCTUnwrap(keypair) - - let bootstrapRelays = load_bootstrap_relays(pubkey: keypair.pubkey) - let relayInfo = RelayInfo(read: true, write: true) - var relays: [String: RelayInfo] = [:] - - for relay in bootstrapRelays { - relays[relay] = relayInfo - } - - let relayJson = encode_json(relays)! - - let tags: [[String]] = pubkeysToPetnames.enumerated().map { - ["p", $0.element.key.description, "", $0.element.value] - } - - return NostrEvent(content: relayJson, keypair: keypair.to_keypair(), kind: NostrKind.contacts.rawValue, tags: tags)! - } - */ - -}