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