diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift index 11410346..d258c9ec 100644 --- a/damus/Views/PostView.swift +++ b/damus/Views/PostView.swift @@ -16,6 +16,7 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex struct PostView: View { @State var post: NSMutableAttributedString = NSMutableAttributedString() + @State var cursor: Int = 0 @FocusState var focus: Bool @State var showPrivateKeyWarning: Bool = false @State var attach_media: Bool = false @@ -104,7 +105,7 @@ struct PostView: View { var TextEntry: some View { ZStack(alignment: .topLeading) { - TextViewWrapper(attributedText: $post) + TextViewWrapper(attributedText: $post, cursor: $cursor) .focused($focus) .textInputAutocapitalization(.sentences) .onChange(of: post) { _ in @@ -191,7 +192,7 @@ struct PostView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - let searching = get_searching_string(post.string) + let searching = get_searching_string(post.string, cursor: cursor) TopBar @@ -204,7 +205,7 @@ struct PostView: View { // This if-block observes @ for tagging if let searching { - UserSearch(damus_state: damus_state, search: searching, post: $post) + UserSearch(damus_state: damus_state, search: searching, post: $post, cursor: $cursor) .frame(maxHeight: .infinity) } else { Divider() @@ -253,8 +254,12 @@ struct PostView: View { } } -func get_searching_string(_ post: String) -> String? { - guard let last_word = post.components(separatedBy: .whitespacesAndNewlines).last else { +func get_searching_string(_ post: String, cursor: Int) -> String? { + guard cursor > 0 else { + return nil + } + + guard let last_word = post[...post.index(post.startIndex, offsetBy: cursor - 1)].components(separatedBy: .whitespacesAndNewlines).last else { return nil } diff --git a/damus/Views/Posting/UserSearch.swift b/damus/Views/Posting/UserSearch.swift index 9964fc23..2420cd88 100644 --- a/damus/Views/Posting/UserSearch.swift +++ b/damus/Views/Posting/UserSearch.swift @@ -22,6 +22,7 @@ struct UserSearch: View { let search: String @Binding var post: NSMutableAttributedString + @Binding var cursor: Int var users: [SearchedUser] { guard let contacts = damus_state.contacts.event else { @@ -36,38 +37,57 @@ struct UserSearch: View { return } - // Remove all characters after the last '@' - removeCharactersAfterLastAtSymbol() + // Remove all characters after the '@' and before the cursor + let newCursor = removeCharactersAfterAtSymbol() // Create and append the user tag let tagAttributedString = createUserTag(for: user, with: pk) - appendUserTag(tagAttributedString) + insertUserTag(tagAttributedString, cursor: newCursor) + + cursor = newCursor } - private func removeCharactersAfterLastAtSymbol() { - while post.string.last != "@" { - post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1)) + private func removeCharactersAfterAtSymbol() -> Int { + let newCursor = cursor + + guard newCursor > 0 else { + return 0 } - post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1)) + + var atSymbolOffset = newCursor + while atSymbolOffset > 0 && post.string[post.string.index(post.string.startIndex, offsetBy: atSymbolOffset - 1)] != "@" { + atSymbolOffset -= 1 + } + + var endOfWordOffset = newCursor + while endOfWordOffset < post.string.count && !post.string[post.string.index(post.string.startIndex, offsetBy: endOfWordOffset)].isWhitespace { + endOfWordOffset += 1 + } + + post.deleteCharacters(in: NSRange(location: atSymbolOffset - 1, length: endOfWordOffset - atSymbolOffset + 1)) + + return atSymbolOffset - 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} " + let tagString = "\u{200B}@\(name)\u{200B} " let tagAttributedString = NSMutableAttributedString(string: tagString, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0), NSAttributedString.Key.link: "@\(pk)"]) + tagAttributedString.removeAttribute(.link, range: NSRange(location: 0, length: 1)) tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2)) + tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: 0, length: 1)) tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2)) return tagAttributedString } - private func appendUserTag(_ tagAttributedString: NSMutableAttributedString) { + private func insertUserTag(_ tagAttributedString: NSMutableAttributedString, cursor: Int) { let mutableString = NSMutableAttributedString() mutableString.append(post) - mutableString.append(tagAttributedString) + mutableString.insert(tagAttributedString, at: cursor) post = mutableString } @@ -88,9 +108,10 @@ struct UserSearch: View { struct UserSearch_Previews: PreviewProvider { static let search: String = "jb55" @State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55") + @State static var cursor: Int = 0 static var previews: some View { - UserSearch(damus_state: test_damus_state(), search: search, post: $post) + UserSearch(damus_state: test_damus_state(), search: search, post: $post, cursor: $cursor) } } diff --git a/damus/Views/TextViewWrapper.swift b/damus/Views/TextViewWrapper.swift index 2345966e..32786dd4 100644 --- a/damus/Views/TextViewWrapper.swift +++ b/damus/Views/TextViewWrapper.swift @@ -9,6 +9,7 @@ import SwiftUI struct TextViewWrapper: UIViewRepresentable { @Binding var attributedText: NSMutableAttributedString + @Binding var cursor: Int func makeUIView(context: Context) -> UITextView { let textView = UITextView() @@ -31,18 +32,21 @@ struct TextViewWrapper: UIViewRepresentable { } func makeCoordinator() -> Coordinator { - Coordinator(attributedText: $attributedText) + Coordinator(attributedText: $attributedText, cursor: $cursor) } class Coordinator: NSObject, UITextViewDelegate { @Binding var attributedText: NSMutableAttributedString + @Binding var cursor: Int - init(attributedText: Binding) { + init(attributedText: Binding, cursor: Binding) { _attributedText = attributedText + _cursor = cursor } func textViewDidChange(_ textView: UITextView) { attributedText = NSMutableAttributedString(attributedString: textView.attributedText) + cursor = textView.selectedRange.upperBound } } }