UserSearch.swift (5706B)
1 // 2 // UserSearch.swift 3 // damus 4 // 5 // Created by William Casarin on 2023-01-28. 6 // 7 8 import SwiftUI 9 10 struct UserSearch: View { 11 let damus_state: DamusState 12 let search: String 13 @Binding var focusWordAttributes: (String?, NSRange?) 14 @Binding var newCursorIndex: Int? 15 16 @Binding var post: NSMutableAttributedString 17 @EnvironmentObject var tagModel: TagModel 18 19 var users: [Pubkey] { 20 guard let txn = NdbTxn(ndb: damus_state.ndb) else { return [] } 21 return search_profiles(profiles: damus_state.profiles, search: search, txn: txn).sorted { a, b in 22 let aFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: a)?.priority ?? 0 23 let bFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: b)?.priority ?? 0 24 25 if aFriendTypePriority > bFriendTypePriority { 26 // `a` should be sorted before `b` 27 return true 28 } else { 29 return false 30 } 31 } 32 } 33 34 func on_user_tapped(pk: Pubkey) { 35 let profile_txn = damus_state.profiles.lookup(id: pk) 36 let profile = profile_txn?.unsafeUnownedValue 37 let user_tag = user_tag_attr_string(profile: profile, pubkey: pk) 38 39 appendUserTag(withTag: user_tag) 40 } 41 42 private func appendUserTag(withTag tag: NSMutableAttributedString) { 43 guard let wordRange = focusWordAttributes.1 else { return } 44 45 let appended = append_user_tag(tag: tag, post: post, word_range: wordRange) 46 self.post = appended.post 47 48 // adjust cursor position appropriately: ('diff' used in TextViewWrapper / updateUIView after below update of 'post') 49 tagModel.diff = appended.tag.length - wordRange.length 50 51 focusWordAttributes = (nil, nil) 52 newCursorIndex = wordRange.location + appended.tag.length 53 } 54 55 var body: some View { 56 VStack(spacing: 0) { 57 Divider() 58 ScrollView { 59 LazyVStack { 60 if users.count == 0 { 61 EmptyUserSearchView() 62 } else { 63 ForEach(users) { pk in 64 UserView(damus_state: damus_state, pubkey: pk) 65 .contentShape(Rectangle()) 66 .onTapGesture { 67 on_user_tapped(pk: pk) 68 } 69 } 70 } 71 } 72 .padding() 73 } 74 } 75 } 76 77 } 78 79 struct UserSearch_Previews: PreviewProvider { 80 static let search: String = "jb55" 81 @State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55") 82 @State static var word: (String?, NSRange?) = (nil, nil) 83 @State static var newCursorIndex: Int? 84 85 static var previews: some View { 86 UserSearch(damus_state: test_damus_state, search: search, focusWordAttributes: $word, newCursorIndex: $newCursorIndex, post: $post) 87 } 88 } 89 90 /// Pad an attributed string: `@jb55` -> ` @jb55 ` 91 func pad_attr_string(tag: NSAttributedString, before: Bool = true) -> NSAttributedString { 92 let new_tag = NSMutableAttributedString(string: "") 93 if before { 94 new_tag.append(.init(string: " ")) 95 } 96 97 new_tag.append(tag) 98 new_tag.append(.init(string: " ")) 99 return new_tag 100 } 101 102 /// Checks if whitespace precedes a tag. Useful to add spacing if we don't have it. 103 func should_prepad_tag(tag: NSAttributedString, post: NSMutableAttributedString, word_range: NSRange) -> Bool { 104 if word_range.location == 0 { // If the range starts at the very beginning of the post, there's nothing preceding it. 105 return false 106 } 107 108 // Range for the character preceding the tag 109 let precedingCharacterRange = NSRange(location: word_range.location - 1, length: 1) 110 111 // Get the preceding character 112 let precedingCharacter = post.attributedSubstring(from: precedingCharacterRange) 113 114 guard let char = precedingCharacter.string.first else { 115 return false 116 } 117 118 if char.isNewline { 119 return false 120 } 121 122 // Check if the preceding character is a whitespace character 123 return !char.isWhitespace 124 } 125 126 struct AppendedTag { 127 let post: NSMutableAttributedString 128 let tag: NSAttributedString 129 } 130 131 /// Appends a user tag (eg: @jb55) to a post. This handles adding additional padding as well. 132 func append_user_tag(tag: NSAttributedString, post: NSMutableAttributedString, word_range: NSRange) -> AppendedTag { 133 let new_post = NSMutableAttributedString(attributedString: post) 134 135 // If we have a non-empty post and the last character is not whitespace, append a space 136 // This prevents issues such as users typing cc@will and have it expand to ccnostr:bech32... 137 let should_prepad = should_prepad_tag(tag: tag, post: post, word_range: word_range) 138 let tag = pad_attr_string(tag: tag, before: should_prepad) 139 140 new_post.replaceCharacters(in: word_range, with: tag) 141 142 return AppendedTag(post: new_post, tag: tag) 143 } 144 145 /// Generate a mention attributed string, including the internal damus:nostr: link 146 func user_tag_attr_string(profile: Profile?, pubkey: Pubkey) -> NSMutableAttributedString { 147 let display_name = Profile.displayName(profile: profile, pubkey: pubkey) 148 let name = display_name.username.truncate(maxLength: 50) 149 let tagString = "@\(name)" 150 151 return NSMutableAttributedString(string: tagString, attributes: [ 152 NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0), 153 NSAttributedString.Key.foregroundColor: UIColor.label, 154 NSAttributedString.Key.link: "damus:nostr:\(pubkey.npub)" 155 ]) 156 } 157