damus

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

commit 1533be77d8e8b8e8546ebc4b4def67c6dcbe6de0
parent c05223ca2b651ffa0b98b209751e832515c395d5
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 15 Mar 2023 08:44:03 -0600

Extend user tagging search to all local profiles

Changelog-Added: Extend user tagging search to all local profiles
Changelog-Fixed: Show @ mentions for users with display_names and no username
Changelog-Fixed: Make user search case insensitive

Diffstat:
Mdamus/Views/Posting/UserSearch.swift | 54+++++++++++++++++++++++++++++++++++++++++++-----------
Mdamus/Views/SearchResultsView.swift | 46+++++++++++++++++++++++++++++-----------------
2 files changed, 72 insertions(+), 28 deletions(-)

diff --git a/damus/Views/Posting/UserSearch.swift b/damus/Views/Posting/UserSearch.swift @@ -25,10 +25,10 @@ struct UserSearch: View { var users: [SearchedUser] { guard let contacts = damus_state.contacts.event else { - return [] + return search_profiles(profiles: damus_state.profiles, search: search) } - return search_users(profiles: damus_state.profiles, tags: contacts.tags, search: search) + return search_users_for_autocomplete(profiles: damus_state.profiles, tags: contacts.tags, search: search) } func on_user_tapped(user: SearchedUser) { @@ -36,21 +36,35 @@ struct UserSearch: View { return } + // Remove all characters after the last '@' + removeCharactersAfterLastAtSymbol() + + // Create and append the user tag + let tagAttributedString = createUserTag(for: user, with: pk) + appendUserTag(tagAttributedString) + } + + private func removeCharactersAfterLastAtSymbol() { while post.string.last != "@" { post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1)) } post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1)) + } + private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString { + let name = Profile.displayName(profile: user.profile, pubkey: pk).username + let tagString = "@\(name)\u{200B} " - var tagString = "" - if let name = user.profile?.name { - tagString = "@\(name)\u{200B} " - } let tagAttributedString = NSMutableAttributedString(string: tagString, - attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0), - NSAttributedString.Key.link: "@\(pk)"]) + attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0), + NSAttributedString.Key.link: "@\(pk)"]) tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2)) tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2)) + + return tagAttributedString + } + + private func appendUserTag(_ tagAttributedString: NSMutableAttributedString) { let mutableString = NSMutableAttributedString() mutableString.append(post) mutableString.append(tagAttributedString) @@ -81,11 +95,11 @@ struct UserSearch_Previews: PreviewProvider { } -func search_users(profiles: Profiles, tags: [[String]], search _search: String) -> [SearchedUser] { +func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search _search: String) -> [SearchedUser] { var seen_user = Set<String>() let search = _search.lowercased() - return tags.reduce(into: Array<SearchedUser>()) { arr, tag in + var matches = tags.reduce(into: Array<SearchedUser>()) { arr, tag in guard tag.count >= 2 && tag[0] == "p" else { return } @@ -103,11 +117,29 @@ func search_users(profiles: Profiles, tags: [[String]], search _search: String) let profile = profiles.lookup(id: pubkey) - guard ((petname?.lowercased().hasPrefix(search) ?? false) || (profile?.name?.lowercased().hasPrefix(search) ?? false)) else { + 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.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 } diff --git a/damus/Views/SearchResultsView.swift b/damus/Views/SearchResultsView.swift @@ -8,7 +8,7 @@ import SwiftUI enum Search { - case profiles([(String, Profile)]) + case profiles([SearchedUser]) case hashtag(String) case profile(String) case note(String) @@ -21,7 +21,7 @@ struct SearchResultsView: View { @State var result: Search? = nil - func ProfileSearchResult(pk: String, res: Profile) -> some View { + func ProfileSearchResult(pk: String) -> some View { FollowUserView(target: .pubkey(pk), damus_state: damus_state) } @@ -31,8 +31,8 @@ struct SearchResultsView: View { switch result { case .profiles(let results): LazyVStack { - ForEach(results, id: \.0) { prof in - ProfileSearchResult(pk: prof.0, res: prof.1) + ForEach(results) { prof in + ProfileSearchResult(pk: prof.pubkey) } } case .hashtag(let ht): @@ -119,22 +119,34 @@ func search_for_string(profiles: Profiles, _ new: String) -> Search? { return .profiles(search_profiles(profiles: profiles, search: new)) } -func search_profiles(profiles: Profiles, search new: String) -> [(String, Profile)] { +func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] { + let new = search.lowercased() return profiles.profiles.enumerated().reduce(into: []) { acc, els in let pk = els.element.key let prof = els.element.value.profile - 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 { - acc.append((pk, prof)) + + 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 +}