damus

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

commit c35331ceda3f12f6d15ffea0a5672197ed399d23
parent 88ddb70ca821e9bfa803d898a988d167c28b37d9
Author: Swift <scoder1747@gmail.com>
Date:   Sat,  6 May 2023 23:12:59 -0400

Fix bug where you could only mention users at the end of a post

Changelog-Fixed: Fix bug where you could only mention users at the end of a post
Closes: #1102

Diffstat:
Mdamus/Views/PostView.swift | 27++++++++++++++++-----------
Mdamus/Views/Posting/UserSearch.swift | 27+++++++++++++++------------
Mdamus/Views/TextViewWrapper.swift | 41+++++++++++++++++++++++++++++++++++++++--
3 files changed, 70 insertions(+), 25 deletions(-)

diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift @@ -43,6 +43,8 @@ struct PostView: View { @State var image_upload_confirm: Bool = false @State var originalReferences: [ReferencedId] = [] @State var references: [ReferencedId] = [] + @State var focusWordAttributes: (String?, NSRange?) = (nil, nil) + @State var newCursorIndex: Int? @State var mediaToUpload: MediaUpload? = nil @@ -203,7 +205,10 @@ struct PostView: View { var TextEntry: some View { ZStack(alignment: .topLeading) { - TextViewWrapper(attributedText: $post) + TextViewWrapper(attributedText: $post, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in + focusWordAttributes = (word, range) + self.newCursorIndex = nil + }) .focused($focus) .textInputAutocapitalization(.sentences) .onChange(of: post) { p in @@ -312,8 +317,7 @@ struct PostView: View { var body: some View { GeometryReader { (deviceSize: GeometryProxy) in VStack(alignment: .leading, spacing: 0) { - - let searching = get_searching_string(post.string) + let searching = get_searching_string(focusWordAttributes.0) TopBar @@ -333,7 +337,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, focusWordAttributes: $focusWordAttributes, newCursorIndex: $newCursorIndex, post: $post) .frame(maxHeight: .infinity) } else { Divider() @@ -412,25 +416,26 @@ struct PostView: View { } } -func get_searching_string(_ post: String) -> String? { - guard let last_word = post.components(separatedBy: .whitespacesAndNewlines).last else { +func get_searching_string(_ word: String?) -> String? { + guard let word = word else { return nil } - - guard last_word.count >= 2 else { + + guard word.count >= 2 else { return nil } - guard last_word.first! == "@" else { + guard let firstCharacter = word.first, + firstCharacter == "@" else { return nil } // don't include @npub... strings - guard last_word.count != 64 else { + guard word.count != 64 else { return nil } - return String(last_word.dropFirst()) + return String(word.dropFirst()) } struct PostView_Previews: PreviewProvider { diff --git a/damus/Views/Posting/UserSearch.swift b/damus/Views/Posting/UserSearch.swift @@ -20,6 +20,8 @@ struct SearchedUser: Identifiable { struct UserSearch: View { let damus_state: DamusState let search: String + @Binding var focusWordAttributes: (String?, NSRange?) + @Binding var newCursorIndex: Int? @Binding var post: NSMutableAttributedString @@ -35,20 +37,19 @@ struct UserSearch: View { guard let pk = bech32_pubkey(user.pubkey) else { return } - - // Remove all characters after the last '@' - removeCharactersAfterLastAtSymbol() - - // Create and append the user tag let tagAttributedString = createUserTag(for: user, with: pk) - appendUserTag(tagAttributedString) + appendUserTag(withTag: tagAttributedString) } - - private func removeCharactersAfterLastAtSymbol() { - while post.string.last != "@" { - post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1)) + + private func appendUserTag(withTag tagAttributedString: NSMutableAttributedString) { + guard let wordRange = focusWordAttributes.1 else { + return } - post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1)) + let mutableString = NSMutableAttributedString(attributedString: post) + mutableString.replaceCharacters(in: wordRange, with: tagAttributedString) + post = mutableString + focusWordAttributes = (nil, nil) + newCursorIndex = wordRange.location + tagAttributedString.string.count } private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString { @@ -97,9 +98,11 @@ struct UserSearch: View { struct UserSearch_Previews: PreviewProvider { static let search: String = "jb55" @State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55") + @State static var word: (String?, NSRange?) = (nil, nil) + @State static var newCursorIndex: Int? static var previews: some View { - UserSearch(damus_state: test_damus_state(), search: search, post: $post) + UserSearch(damus_state: test_damus_state(), search: search, focusWordAttributes: $word, newCursorIndex: $newCursorIndex, post: $post) } } diff --git a/damus/Views/TextViewWrapper.swift b/damus/Views/TextViewWrapper.swift @@ -9,6 +9,8 @@ import SwiftUI struct TextViewWrapper: UIViewRepresentable { @Binding var attributedText: NSMutableAttributedString + let cursorIndex: Int? + var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil func makeUIView(context: Context) -> UITextView { let textView = UITextView() @@ -29,21 +31,56 @@ struct TextViewWrapper: UIViewRepresentable { func updateUIView(_ uiView: UITextView, context: Context) { uiView.attributedText = attributedText TextViewWrapper.setTextProperties(uiView) + setCursorPosition(textView: uiView) + } + + private func setCursorPosition(textView: UITextView) { + guard let index = cursorIndex, let newPosition = textView.position(from: textView.beginningOfDocument, offset: index) else { + return + } + textView.selectedTextRange = textView.textRange(from: newPosition, to: newPosition) } func makeCoordinator() -> Coordinator { - Coordinator(attributedText: $attributedText) + Coordinator(attributedText: $attributedText, getFocusWordForMention: getFocusWordForMention) } class Coordinator: NSObject, UITextViewDelegate { @Binding var attributedText: NSMutableAttributedString + var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil - init(attributedText: Binding<NSMutableAttributedString>) { + init(attributedText: Binding<NSMutableAttributedString>, getFocusWordForMention: ((String?, NSRange?) -> Void)?) { _attributedText = attributedText + self.getFocusWordForMention = getFocusWordForMention } func textViewDidChange(_ textView: UITextView) { attributedText = NSMutableAttributedString(attributedString: textView.attributedText) + processFocusedWordForMention(textView: textView) + } + + private func processFocusedWordForMention(textView: UITextView) { + if let selectedRange = textView.selectedTextRange { + var val: (String?, NSRange?) + if let wordRange = textView.tokenizer.rangeEnclosingPosition(selectedRange.start, with: .word, inDirection: .init(rawValue: UITextLayoutDirection.left.rawValue)) { + if let startPosition = textView.position(from: wordRange.start, offset: -1), + let cursorPosition = textView.position(from: selectedRange.start, offset: 0) { + let word = textView.text(in: textView.textRange(from: startPosition, to: cursorPosition)!) + val = (word, convertToNSRange(startPosition, cursorPosition, textView)) + } + } + getFocusWordForMention?(val.0, val.1) + } + } + + private func convertToNSRange( _ startPosition: UITextPosition, _ endPosition: UITextPosition, _ textView: UITextView) -> NSRange? { + let startOffset = textView.offset(from: textView.beginningOfDocument, to: startPosition) + let endOffset = textView.offset(from: textView.beginningOfDocument, to: endPosition) + let length = endOffset - startOffset + guard length >= 0, startOffset >= 0 else { + return nil + } + return NSRange(location: startOffset, length: length) } } }