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
This commit is contained in:
@@ -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 word.count >= 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard last_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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user