Compare commits
1 Commits
trie
...
nip56-conf
| Author | SHA1 | Date | |
|---|---|---|---|
|
16c839afa6
|
@@ -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 */; };
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
||||
@@ -381,8 +377,6 @@
|
||||
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A5CAE1F298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A5E47C42A4A6CF400C0D090 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
|
||||
3A5E47C62A4A76C800C0D090 /* TrieTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrieTests.swift; sourceTree = "<group>"; };
|
||||
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -396,8 +390,6 @@
|
||||
3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtil.swift; sourceTree = "<group>"; };
|
||||
3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSearchCache.swift; sourceTree = "<group>"; };
|
||||
3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearchCacheTests.swift; sourceTree = "<group>"; };
|
||||
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
@@ -929,8 +921,6 @@
|
||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
|
||||
4C7D09772A0B0CC900943473 /* WalletModel.swift */,
|
||||
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */,
|
||||
3A5E47C42A4A6CF400C0D090 /* Trie.swift */,
|
||||
3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -1389,8 +1379,6 @@
|
||||
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
|
||||
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */,
|
||||
3AFBF3FC29FDA7CC00E79C7C /* CustomZapViewTests.swift */,
|
||||
3A5E47C62A4A76C800C0D090 /* TrieTests.swift */,
|
||||
3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */,
|
||||
);
|
||||
path = damusTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -1743,7 +1731,6 @@
|
||||
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
|
||||
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */,
|
||||
3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */,
|
||||
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
|
||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
||||
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
|
||||
@@ -1880,7 +1867,6 @@
|
||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
|
||||
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
|
||||
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
|
||||
3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */,
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
||||
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
|
||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||
@@ -1972,7 +1958,6 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */,
|
||||
3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */,
|
||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
||||
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
|
||||
@@ -1987,7 +1972,6 @@
|
||||
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
||||
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */,
|
||||
F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */,
|
||||
3A5E47C72A4A76C800C0D090 /* TrieTests.swift in Sources */,
|
||||
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
|
||||
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
|
||||
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
|
||||
|
||||
@@ -624,14 +624,13 @@ struct ContentView: View {
|
||||
let nwc = WalletConnectURL(str: nwc_str) {
|
||||
try? pool.add_relay(.nwc(url: nwc.relay))
|
||||
}
|
||||
|
||||
let user_search_cache = UserSearchCache()
|
||||
|
||||
self.damus_state = DamusState(pool: pool,
|
||||
keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
profiles: Profiles(user_search_cache: user_search_cache),
|
||||
profiles: Profiles(),
|
||||
dms: home.dms,
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: pubkey),
|
||||
@@ -647,8 +646,7 @@ struct ContentView: View {
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
muted_threads: MutedThreadsManager(keypair: keypair),
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: self.navigationCoordinator,
|
||||
user_search_cache: user_search_cache
|
||||
nav: self.navigationCoordinator
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
@@ -878,18 +876,17 @@ func handle_unfollow(state: DamusState, notif: Notification) {
|
||||
|
||||
let target = notif.object as! FollowTarget
|
||||
let pk = target.pubkey
|
||||
let old_contacts = state.contacts.event
|
||||
|
||||
if let ev = unfollow_user(postbox: state.postbox,
|
||||
our_contacts: old_contacts,
|
||||
our_contacts: state.contacts.event,
|
||||
pubkey: state.pubkey,
|
||||
privkey: privkey,
|
||||
unfollow: pk) {
|
||||
notify(.unfollowed, pk)
|
||||
|
||||
|
||||
state.contacts.event = ev
|
||||
state.contacts.remove_friend(pk)
|
||||
state.user_search_cache.updateOwnContactsPetnames(id: state.pubkey, oldEvent: old_contacts, newEvent: ev)
|
||||
//friend_events = friend_events.filter { $0.pubkey != pk }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ struct DamusState {
|
||||
let muted_threads: MutedThreadsManager
|
||||
let wallet: WalletModel
|
||||
let nav: NavigationCoordinator
|
||||
let user_search_cache: UserSearchCache
|
||||
|
||||
@discardableResult
|
||||
func add_zap(zap: Zapping) -> Bool {
|
||||
@@ -59,6 +58,5 @@ struct DamusState {
|
||||
}
|
||||
|
||||
static var empty: DamusState {
|
||||
let user_search_cache = UserSearchCache()
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(user_search_cache: user_search_cache), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore()), nav: NavigationCoordinator(), user_search_cache: user_search_cache) }
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore()), nav: NavigationCoordinator()) }
|
||||
}
|
||||
|
||||
@@ -612,8 +612,7 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
|
||||
contacts.add_friend_contact(ev)
|
||||
}
|
||||
|
||||
func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
let contacts = state.contacts
|
||||
func load_our_contacts(contacts: Contacts, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
var new_pks = Set<String>()
|
||||
// our contacts
|
||||
for tag in ev.tags {
|
||||
@@ -642,8 +641,6 @@ func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent)
|
||||
contacts.remove_friend(pk)
|
||||
}
|
||||
}
|
||||
|
||||
state.user_search_cache.updateOwnContactsPetnames(id: contacts.our_pubkey, oldEvent: m_old_ev, newEvent: ev)
|
||||
}
|
||||
|
||||
|
||||
@@ -704,9 +701,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
||||
}
|
||||
|
||||
var old_nip05: String? = nil
|
||||
let mprof = profiles.lookup_with_timestamp(id: ev.pubkey)
|
||||
|
||||
if let mprof {
|
||||
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||
old_nip05 = mprof.profile.nip05
|
||||
if mprof.event.created_at > ev.created_at {
|
||||
// skip if we already have an newer profile
|
||||
@@ -815,7 +810,7 @@ func load_our_stuff(state: DamusState, ev: NostrEvent) {
|
||||
let m_old_ev = state.contacts.event
|
||||
state.contacts.event = ev
|
||||
|
||||
load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev)
|
||||
load_our_contacts(contacts: state.contacts, m_old_ev: m_old_ev, ev: ev)
|
||||
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,27 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ReportType: String {
|
||||
case explicit
|
||||
case illegal
|
||||
enum ReportType: String, CustomStringConvertible, CaseIterable {
|
||||
case spam
|
||||
case nudity
|
||||
case profanity
|
||||
case illegal
|
||||
case impersonation
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .spam:
|
||||
return NSLocalizedString("Spam", comment: "Description of report type for spam.")
|
||||
case .nudity:
|
||||
return NSLocalizedString("Nudity", comment: "Description of report type for nudity.")
|
||||
case .profanity:
|
||||
return NSLocalizedString("Profanity", comment: "Description of report type for profanity.")
|
||||
case .illegal:
|
||||
return NSLocalizedString("Illegal Content", comment: "Description of report type for illegal content.")
|
||||
case .impersonation:
|
||||
return NSLocalizedString("Impersonation", comment: "Description of report type for impersonation.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ReportNoteTarget {
|
||||
@@ -31,16 +47,12 @@ struct Report {
|
||||
}
|
||||
|
||||
func create_report_tags(target: ReportTarget, type: ReportType) -> [[String]] {
|
||||
var tags: [[String]]
|
||||
switch target {
|
||||
case .user(let pubkey):
|
||||
tags = [["p", pubkey]]
|
||||
return [["p", pubkey, type.rawValue]]
|
||||
case .note(let notet):
|
||||
tags = [["e", notet.note_id], ["p", notet.pubkey]]
|
||||
return [["e", notet.note_id, type.rawValue], ["p", notet.pubkey]]
|
||||
}
|
||||
|
||||
tags.append(["report", type.rawValue])
|
||||
return tags
|
||||
}
|
||||
|
||||
func create_report_event(privkey: String, report: Report) -> NostrEvent? {
|
||||
|
||||
@@ -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<V: Hashable> {
|
||||
private var children: [Character : Trie] = [:]
|
||||
|
||||
/// Separate exact matches from strict substrings so that exact matches appear first in returned results.
|
||||
private var exactMatchValues = Set<V>()
|
||||
private var substringMatchValues = Set<V>()
|
||||
|
||||
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 exactMatches = Set<V>()
|
||||
var substringMatches = Set<V>()
|
||||
var queue = [currentNode]
|
||||
|
||||
while !queue.isEmpty {
|
||||
let node = queue.removeFirst()
|
||||
exactMatches.formUnion(node.exactMatchValues)
|
||||
substringMatches.formUnion(node.substringMatchValues)
|
||||
queue.append(contentsOf: node.children.values)
|
||||
}
|
||||
|
||||
return Array(exactMatches) + substringMatches
|
||||
}
|
||||
|
||||
/// 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..<key.count {
|
||||
var currentNode = self
|
||||
|
||||
// Find branch with matching prefix.
|
||||
for char in key[key.index(key.startIndex, offsetBy: i)...] {
|
||||
if let child = currentNode.children[char] {
|
||||
currentNode = child
|
||||
} else {
|
||||
let child = Trie()
|
||||
child.parent = currentNode
|
||||
currentNode.children[char] = child
|
||||
currentNode = child
|
||||
}
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
currentNode.exactMatchValues.insert(value)
|
||||
} else {
|
||||
currentNode.substringMatchValues.insert(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes value of type V from this trie for the specified key.
|
||||
func remove(key: String, value: V) {
|
||||
for i in 0..<key.count {
|
||||
var currentNode = self
|
||||
|
||||
var foundLeafNode = true
|
||||
|
||||
// Find branch with matching prefix.
|
||||
for j in i..<key.count {
|
||||
let char = key[key.index(key.startIndex, offsetBy: j)]
|
||||
|
||||
if let child = currentNode.children[char] {
|
||||
currentNode = child
|
||||
} else {
|
||||
foundLeafNode = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundLeafNode {
|
||||
currentNode.exactMatchValues.remove(value)
|
||||
currentNode.substringMatchValues.remove(value)
|
||||
|
||||
// Clean up the tree if this leaf node no longer holds values or children.
|
||||
for j in (i..<key.count).reversed() {
|
||||
if let parent = currentNode.parent, !currentNode.hasValues && !currentNode.hasChildren {
|
||||
currentNode = parent
|
||||
let char = key[key.index(key.startIndex, offsetBy: j)]
|
||||
currentNode.children.removeValue(forKey: char)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
//
|
||||
// UserSearchCache.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 6/27/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Cache of searchable users by name, display_name, NIP-05 identifier, or own contact list petname.
|
||||
/// Optimized for fast searches of substrings by using a Trie.
|
||||
/// Optimal for performing user searches that could be initiated by typing quickly on a keyboard into a text input field.
|
||||
class UserSearchCache {
|
||||
private let trie = Trie<String>()
|
||||
|
||||
func search(key: String) -> [String] {
|
||||
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.
|
||||
func updateProfile(id: String, 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.
|
||||
private func addProfile(id: String, 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: String, oldEvent: NostrEvent?, newEvent: NostrEvent) {
|
||||
guard newEvent.known_kind == .contacts && newEvent.pubkey == id else {
|
||||
return
|
||||
}
|
||||
|
||||
var petnames: [String: String] = [:]
|
||||
|
||||
// Gets all petnames from our new contacts list.
|
||||
newEvent.tags.forEach { tag in
|
||||
guard tag.count >= 4 && tag[0] == "p" else {
|
||||
return
|
||||
}
|
||||
|
||||
let pubkey = tag[1]
|
||||
let petname = tag[3]
|
||||
|
||||
petnames[pubkey] = petname
|
||||
}
|
||||
|
||||
// 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 {
|
||||
oldEvent.tags.forEach { tag in
|
||||
guard tag.count >= 4 && tag[0] == "p" else {
|
||||
return
|
||||
}
|
||||
|
||||
let pubkey = tag[1]
|
||||
let oldPetname = tag[3]
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,6 @@ class Profiles {
|
||||
var zappers: [String: String] = [:]
|
||||
|
||||
private let database = ProfileDatabase()
|
||||
|
||||
let user_search_cache: UserSearchCache
|
||||
|
||||
init(user_search_cache: UserSearchCache) {
|
||||
self.user_search_cache = user_search_cache
|
||||
}
|
||||
|
||||
func is_validated(_ pk: String) -> NIP05? {
|
||||
validated[pk]
|
||||
@@ -46,9 +40,7 @@ class Profiles {
|
||||
|
||||
func add(id: String, profile: TimestampedProfile) {
|
||||
queue.async(flags: .barrier) {
|
||||
let old_timestamped_profile = self.profiles[id]
|
||||
self.profiles[id] = profile
|
||||
self.user_search_cache.updateProfile(id: id, profiles: self, oldProfile: old_timestamped_profile?.profile, newProfile: profile.profile)
|
||||
}
|
||||
|
||||
Task {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SearchedUser: Identifiable {
|
||||
let petname: String?
|
||||
let profile: Profile?
|
||||
let pubkey: String
|
||||
|
||||
@@ -27,7 +28,11 @@ struct UserSearch: View {
|
||||
@EnvironmentObject var tagModel: TagModel
|
||||
|
||||
var users: [SearchedUser] {
|
||||
return search_profiles(profiles: damus_state.profiles, search: search)
|
||||
guard let contacts = damus_state.contacts.event else {
|
||||
return search_profiles(profiles: damus_state.profiles, search: search)
|
||||
}
|
||||
|
||||
return search_users_for_autocomplete(profiles: damus_state.profiles, tags: contacts.tags, search: search)
|
||||
}
|
||||
|
||||
func on_user_tapped(user: SearchedUser) {
|
||||
@@ -94,6 +99,56 @@ struct UserSearch_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search _search: String) -> [SearchedUser] {
|
||||
var seen_user = Set<String>()
|
||||
let search = _search.lowercased()
|
||||
|
||||
var matches = tags.reduce(into: Array<SearchedUser>()) { arr, tag in
|
||||
guard tag.count >= 2 && tag[0] == "p" else {
|
||||
return
|
||||
}
|
||||
|
||||
let pubkey = tag[1]
|
||||
guard !seen_user.contains(pubkey) else {
|
||||
return
|
||||
}
|
||||
seen_user.insert(pubkey)
|
||||
|
||||
var petname: String? = nil
|
||||
if tag.count >= 4 {
|
||||
petname = tag[3]
|
||||
}
|
||||
|
||||
let profile = profiles.lookup(id: pubkey)
|
||||
|
||||
guard ((petname?.lowercased().hasPrefix(search) ?? false) ||
|
||||
(profile?.name?.lowercased().hasPrefix(search) ?? false) ||
|
||||
(profile?.display_name?.lowercased().hasPrefix(search) ?? false)) else {
|
||||
return
|
||||
}
|
||||
|
||||
let searched_user = SearchedUser(petname: petname, profile: profile, pubkey: pubkey)
|
||||
arr.append(searched_user)
|
||||
}
|
||||
|
||||
// search profile cache as well
|
||||
for tup in profiles.enumerated() {
|
||||
let pk = tup.element.key
|
||||
let prof = tup.element.value.profile
|
||||
|
||||
guard !seen_user.contains(pk) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let match = profile_search_matches(profiles: profiles, profile: prof, pubkey: pk, search: search) {
|
||||
matches.append(match)
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
func user_tag_attr_string(profile: Profile?, pubkey: String) -> NSMutableAttributedString {
|
||||
let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
let name = display_name.username.truncate(maxLength: 50)
|
||||
|
||||
@@ -162,8 +162,7 @@ func get_profile_url(picture: String?, pubkey: String, profiles: Profiles) -> UR
|
||||
}
|
||||
|
||||
func make_preview_profiles(_ pubkey: String) -> Profiles {
|
||||
let user_search_cache = UserSearchCache()
|
||||
let profiles = Profiles(user_search_cache: user_search_cache)
|
||||
let profiles = Profiles()
|
||||
let picture = "http://cdn.jb55.com/img/red-me.jpg"
|
||||
let profile = Profile(name: "jb55", display_name: "William Casarin", about: "It's me", picture: picture, banner: "", website: "https://jb55.com", lud06: nil, lud16: nil, nip05: "jb55.com", damus_donation: nil)
|
||||
let ts_profile = TimestampedProfile(profile: profile, timestamp: 0, event: test_event)
|
||||
|
||||
@@ -14,6 +14,8 @@ struct ReportView: View {
|
||||
|
||||
@State var report_sent: Bool = false
|
||||
@State var report_id: String = ""
|
||||
@State var report_message: String = ""
|
||||
@State var selected_report_type: ReportType?
|
||||
|
||||
var body: some View {
|
||||
if report_sent {
|
||||
@@ -43,8 +45,8 @@ struct ReportView: View {
|
||||
.padding()
|
||||
}
|
||||
|
||||
func do_send_report(type: ReportType) {
|
||||
guard let ev = send_report(privkey: privkey, postbox: postbox, target: target, type: type) else {
|
||||
func do_send_report() {
|
||||
guard let selected_report_type, let ev = send_report(privkey: privkey, postbox: postbox, target: target, type: selected_report_type, message: report_message) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -55,6 +57,15 @@ struct ReportView: View {
|
||||
report_sent = true
|
||||
report_id = note_id
|
||||
}
|
||||
|
||||
var send_report_button_text: String {
|
||||
switch target {
|
||||
case .note:
|
||||
return NSLocalizedString("Report Note", comment: "Button to report a note.")
|
||||
case .user:
|
||||
return NSLocalizedString("Report User", comment: "Button to report a user.")
|
||||
}
|
||||
}
|
||||
|
||||
var MainForm: some View {
|
||||
VStack {
|
||||
@@ -65,23 +76,27 @@ struct ReportView: View {
|
||||
|
||||
Form {
|
||||
Section(content: {
|
||||
Button(NSLocalizedString("It's spam", comment: "Button for user to report that the account or content has spam.")) {
|
||||
do_send_report(type: .spam)
|
||||
}
|
||||
|
||||
Button(NSLocalizedString("Nudity or explicit content", comment: "Button for user to report that the account or content has nudity or explicit content.")) {
|
||||
do_send_report(type: .explicit)
|
||||
}
|
||||
|
||||
Button(NSLocalizedString("Illegal content", comment: "Button for user to report that the account or content has illegal content.")) {
|
||||
do_send_report(type: .illegal)
|
||||
}
|
||||
|
||||
if case .user = target {
|
||||
Button(NSLocalizedString("They are impersonating someone", comment: "Button for user to report that the account is impersonating someone.")) {
|
||||
do_send_report(type: .impersonation)
|
||||
Picker("", selection: $selected_report_type) {
|
||||
ForEach(ReportType.allCases, id: \.self) { report_type in
|
||||
// Impersonation type is not supported when reporting notes.
|
||||
if case .note = target, report_type != .impersonation {
|
||||
Text(verbatim: String(describing: report_type))
|
||||
.tag(Optional(report_type))
|
||||
} else if case .user = target {
|
||||
Text(verbatim: String(describing: report_type))
|
||||
.tag(Optional(report_type))
|
||||
}
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.pickerStyle(.inline)
|
||||
|
||||
TextField(NSLocalizedString("Additional information (optional)", comment: "Prompt to enter optional additional information when reporting an account or content."), text: $report_message, axis: .vertical)
|
||||
|
||||
Button(send_report_button_text) {
|
||||
do_send_report()
|
||||
}
|
||||
.disabled(selected_report_type == nil)
|
||||
}, header: {
|
||||
Text("What do you want to report?", comment: "Header text to prompt user what issue they want to report.")
|
||||
}, footer: {
|
||||
@@ -92,8 +107,8 @@ struct ReportView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func send_report(privkey: String, postbox: PostBox, target: ReportTarget, type: ReportType) -> NostrEvent? {
|
||||
let report = Report(type: type, target: target, message: "")
|
||||
func send_report(privkey: String, postbox: PostBox, target: ReportTarget, type: ReportType, message: String) -> NostrEvent? {
|
||||
let report = Report(type: type, target: target, message: message)
|
||||
guard let ev = create_report_event(privkey: privkey, report: report) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -180,21 +180,33 @@ func make_hashtagable(_ str: String) -> String {
|
||||
}
|
||||
|
||||
func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] {
|
||||
// Search by hex pubkey.
|
||||
if search.count == 64 && hex_decode(search) != nil, let profile = profiles.lookup(id: search) {
|
||||
return [SearchedUser(profile: profile, pubkey: search)]
|
||||
}
|
||||
|
||||
// Search by npub pubkey.
|
||||
if search.starts(with: "npub"), let bech32_key = decode_bech32_key(search), case Bech32Key.pub(let hex) = bech32_key, let profile = profiles.lookup(id: hex) {
|
||||
return [SearchedUser(profile: profile, pubkey: hex)]
|
||||
}
|
||||
|
||||
let new = search.lowercased()
|
||||
let matched_pubkeys = profiles.user_search_cache.search(key: new)
|
||||
|
||||
return matched_pubkeys
|
||||
.map { ($0, profiles.lookup(id: $0)) }
|
||||
.filter { $1 != nil }
|
||||
.map { SearchedUser(profile: $1, pubkey: $0) }
|
||||
return profiles.enumerated().reduce(into: []) { acc, els in
|
||||
let pk = els.element.key
|
||||
let prof = els.element.value.profile
|
||||
|
||||
if let searched = profile_search_matches(profiles: profiles, profile: prof, pubkey: pk, search: new) {
|
||||
acc.append(searched)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func profile_search_matches(profiles: Profiles, profile prof: Profile, pubkey pk: String, search new: String) -> SearchedUser? {
|
||||
let lowname = prof.name.map { $0.lowercased() }
|
||||
let lownip05 = profiles.is_validated(pk).map { $0.host.lowercased() }
|
||||
let lowdisp = prof.display_name.map { $0.lowercased() }
|
||||
let ok = new.count == 1 ?
|
||||
((lowname?.starts(with: new) ?? false) ||
|
||||
(lownip05?.starts(with: new) ?? false) ||
|
||||
(lowdisp?.starts(with: new) ?? false)) : (pk.starts(with: new) || String(new.dropFirst()) == pk
|
||||
|| lowname?.contains(new) ?? false
|
||||
|| lownip05?.contains(new) ?? false
|
||||
|| lowdisp?.contains(new) ?? false)
|
||||
|
||||
if ok {
|
||||
return SearchedUser(petname: nil, profile: prof, pubkey: pk)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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<String>()
|
||||
|
||||
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<String>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
//
|
||||
// UserSearchCacheTests.swift
|
||||
// damusTests
|
||||
//
|
||||
// Created by Terry Yiu on 6/30/23.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import damus
|
||||
|
||||
final class UserSearchCacheTests: XCTestCase {
|
||||
|
||||
var keypair: Keypair? = nil
|
||||
let damusState = DamusState.empty
|
||||
let nip05 = "_@somedomain.com"
|
||||
|
||||
override func setUpWithError() throws {
|
||||
keypair = try XCTUnwrap(generate_new_keypair())
|
||||
|
||||
if let keypair {
|
||||
let pubkey = keypair.pubkey
|
||||
|
||||
damusState.profiles.validated[pubkey] = NIP05.parse(nip05)
|
||||
|
||||
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)
|
||||
let timestampedProfile = TimestampedProfile(profile: profile, timestamp: 0, event: test_event)
|
||||
damusState.profiles.add(id: pubkey, profile: timestampedProfile)
|
||||
|
||||
// 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])
|
||||
}
|
||||
|
||||
func testUpdateProfile() throws {
|
||||
let keypair = try XCTUnwrap(keypair)
|
||||
|
||||
let newNip05 = "_@other.xyz"
|
||||
damusState.profiles.validated[keypair.pubkey] = NIP05.parse(newNip05)
|
||||
|
||||
let newProfile = Profile(name: "whoami", display_name: "T-DAWG", about: nil, picture: nil, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: newNip05, damus_donation: nil)
|
||||
let newTimestampedProfile = TimestampedProfile(profile: newProfile, timestamp: 1000, event: test_event)
|
||||
damusState.profiles.add(id: keypair.pubkey, profile: newTimestampedProfile)
|
||||
|
||||
// Lookup to synchronize access on profiles dictionary to avoid race conditions.
|
||||
let _ = damusState.profiles.lookup(id: keypair.pubkey)
|
||||
|
||||
// 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 = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
||||
let jb55 = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
|
||||
|
||||
var pubkeysToPetnames = [String: 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: [String: String]) throws -> NostrEvent {
|
||||
let keypair = try XCTUnwrap(keypair)
|
||||
let privkey = try XCTUnwrap(keypair.privkey)
|
||||
|
||||
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 = pubkeysToPetnames.enumerated().map {
|
||||
["p", $0.element.key, "", $0.element.value]
|
||||
}
|
||||
|
||||
let ev = NostrEvent(content: relayJson,
|
||||
pubkey: keypair.pubkey,
|
||||
kind: NostrKind.contacts.rawValue,
|
||||
tags: tags)
|
||||
ev.calculate_id()
|
||||
ev.sign(privkey: privkey)
|
||||
return ev
|
||||
}
|
||||
|
||||
}
|
||||
@@ -69,7 +69,7 @@ final class ZapTests: XCTestCase {
|
||||
XCTAssertEqual(zap.target, ZapTarget.profile("32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"))
|
||||
|
||||
XCTAssertEqual(zap_notification_title(zap), "Zap")
|
||||
XCTAssertEqual(zap_notification_body(profiles: Profiles(user_search_cache: UserSearchCache()), zap: zap), "You received 1k sats from 107jk7ht:2qlu3nfm")
|
||||
XCTAssertEqual(zap_notification_body(profiles: Profiles(), zap: zap), "You received 1k sats from 107jk7ht:2qlu3nfm")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user