damus

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

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