damus

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

commit 436d20dfbde7f90c6052db2f078d7b65d9ea3cc9
parent 810b3e1fa5fa88a46dafb85acee0d27f79c33e56
Author: Swift <scoder1747@gmail.com>
Date:   Fri, 24 Feb 2023 13:28:47 -0500

Rich tagging

Changelog-Changed: No more inline npubs when tagging users
Closes: #691

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/Models/DraftsModel.swift | 4++--
Mdamus/Views/PostView.swift | 31+++++++++++++++++++------------
Mdamus/Views/Posting/UserSearch.swift | 25++++++++++++++++++++++---
Adamus/Views/TextViewWrapper.swift | 44++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 91 insertions(+), 17 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -219,6 +219,7 @@ 7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */; }; 7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFF6316299FEFE5005D382A /* SelectableText.swift */; }; 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; }; + 9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; }; @@ -548,6 +549,7 @@ 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.swift"; sourceTree = "<group>"; }; 7CFF6316299FEFE5005D382A /* SelectableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableText.swift; sourceTree = "<group>"; }; 9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; }; + 9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = "<group>"; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; }; DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; }; @@ -741,6 +743,7 @@ 4C363A8D28236FE4006E126D /* NoteContentView.swift */, 4C75EFAC28049CFB0006080F /* PostButton.swift */, 4C75EFA327FA577B0006080F /* PostView.swift */, + 9C83F89229A937B900136C08 /* TextViewWrapper.swift */, 4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */, 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */, 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */, @@ -1301,6 +1304,7 @@ E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */, 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */, 4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */, + 9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */, 4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */, 4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */, 4C75EFB128049D510006080F /* NostrResponse.swift in Sources */, diff --git a/damus/Models/DraftsModel.swift b/damus/Models/DraftsModel.swift @@ -8,6 +8,6 @@ import Foundation class Drafts: ObservableObject { - @Published var post: String = "" - @Published var replies: [NostrEvent: String] = [:] + @Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "") + @Published var replies: [NostrEvent: NSMutableAttributedString] = [:] } diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift @@ -15,7 +15,7 @@ enum NostrPostResult { let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Text box prompt to ask user to type their post.") struct PostView: View { - @State var post: String = "" + @State var post: NSMutableAttributedString = NSMutableAttributedString() @FocusState var focus: Bool @State var showPrivateKeyWarning: Bool = false @@ -44,7 +44,14 @@ struct PostView: View { if replying_to?.known_kind == .chat { kind = .chat } - let content = self.post.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + + post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in + if let link = attributes[.link] as? String { + post.replaceCharacters(in: range, with: link) + } + } + + let content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) let new_post = NostrPost(content: content, references: references, kind: kind) NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post)) @@ -52,14 +59,14 @@ struct PostView: View { if let replying_to { damus_state.drafts.replies.removeValue(forKey: replying_to) } else { - damus_state.drafts.post = "" + damus_state.drafts.post = NSMutableAttributedString(string: "") } dismiss() } var is_post_empty: Bool { - return post.allSatisfy { $0.isWhitespace } + return post.string.allSatisfy { $0.isWhitespace } } var body: some View { @@ -74,7 +81,7 @@ struct PostView: View { if !is_post_empty { Button(NSLocalizedString("Post", comment: "Button to post a note.")) { - showPrivateKeyWarning = contentContainsPrivateKey(self.post) + showPrivateKeyWarning = contentContainsPrivateKey(self.post.string) if !showPrivateKeyWarning { self.send_post() @@ -97,7 +104,7 @@ struct PostView: View { VStack(alignment: .leading) { ZStack(alignment: .topLeading) { - TextEditor(text: $post) + TextViewWrapper(attributedText: $post) .focused($focus) .textInputAutocapitalization(.sentences) .onChange(of: post) { _ in @@ -108,7 +115,7 @@ struct PostView: View { } } - if post.isEmpty { + if post.string.isEmpty { Text(POST_PLACEHOLDER) .padding(.top, 8) .padding(.leading, 4) @@ -120,7 +127,7 @@ struct PostView: View { } // This if-block observes @ for tagging - if let searching = get_searching_string(post) { + if let searching = get_searching_string(post.string) { VStack { Spacer() UserSearch(damus_state: damus_state, search: searching, post: $post) @@ -130,7 +137,7 @@ struct PostView: View { .onAppear() { if let replying_to { if damus_state.drafts.replies[replying_to] == nil { - damus_state.drafts.replies[replying_to] = "" + damus_state.drafts.post = NSMutableAttributedString(string: "") } if let p = damus_state.drafts.replies[replying_to] { post = p @@ -144,10 +151,10 @@ struct PostView: View { } } .onDisappear { - if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { damus_state.drafts.replies.removeValue(forKey: replying_to) - } else if replying_to == nil && damus_state.drafts.post.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - damus_state.drafts.post = "" + } else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + damus_state.drafts.post = NSMutableAttributedString(string : "") } } .padding() diff --git a/damus/Views/Posting/UserSearch.swift b/damus/Views/Posting/UserSearch.swift @@ -20,7 +20,8 @@ struct SearchedUser: Identifiable { struct UserSearch: View { let damus_state: DamusState let search: String - @Binding var post: String + + @Binding var post: NSMutableAttributedString var users: [SearchedUser] { guard let contacts = damus_state.contacts.event else { @@ -39,7 +40,25 @@ struct UserSearch: View { guard let pk = bech32_pubkey(user.pubkey) else { return } - post = post.replacingOccurrences(of: "@"+search, with: "@"+pk+" ") + + while post.string.last != "@" { + post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1)) + } + post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1)) + + + var tagString = "" + if let name = user.profile?.name { + tagString = "@\(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: tagAttributedString.length - 2, length: 2)) + let mutableString = NSMutableAttributedString() + mutableString.append(post) + mutableString.append(tagAttributedString) + post = mutableString } } } @@ -49,7 +68,7 @@ struct UserSearch: View { struct UserSearch_Previews: PreviewProvider { static let search: String = "jb55" - @State static var post: String = "some @jb55" + @State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55") static var previews: some View { UserSearch(damus_state: test_damus_state(), search: search, post: $post) diff --git a/damus/Views/TextViewWrapper.swift b/damus/Views/TextViewWrapper.swift @@ -0,0 +1,44 @@ +// +// TextViewWrapper.swift +// damus +// +// Created by Swift on 2/24/23. +// + +import SwiftUI + +struct TextViewWrapper: UIViewRepresentable { + @Binding var attributedText: NSMutableAttributedString + + func makeUIView(context: Context) -> UITextView { + let textView = UITextView() + textView.delegate = context.coordinator + textView.font = UIFont.systemFont(ofSize: 18) + textView.textColor = UIColor.black + let linkAttributes: [NSAttributedString.Key : Any] = [ + NSAttributedString.Key.foregroundColor: UIColor(Color.accentColor)] + textView.linkTextAttributes = linkAttributes + return textView + } + + func updateUIView(_ uiView: UITextView, context: Context) { + uiView.attributedText = attributedText + } + + func makeCoordinator() -> Coordinator { + Coordinator(attributedText: $attributedText) + } + + class Coordinator: NSObject, UITextViewDelegate { + @Binding var attributedText: NSMutableAttributedString + + init(attributedText: Binding<NSMutableAttributedString>) { + _attributedText = attributedText + } + + func textViewDidChange(_ textView: UITextView) { + attributedText = NSMutableAttributedString(attributedString: textView.attributedText) + } + } +} +