damus

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

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