From c09018be488a679cdd4d72d72353b47630c173aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Sat, 17 Aug 2024 15:15:10 -0700 Subject: [PATCH] Add support for adding comments when creating a highlight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changelog-Added: Add support for adding comments when creating a highlight Signed-off-by: Daniel D’Aquino --- damus.xcodeproj/project.pbxproj | 8 +- damus/Components/SelectableText.swift | 9 ++- damus/ContentView.swift | 2 +- damus/Models/DraftsModel.swift | 1 + damus/Models/HighlightEvent.swift | 28 +++++++ damus/Models/Mentions.swift | 9 --- damus/Models/Post.swift | 34 +++++++- .../Highlight/HighlightDraftContentView.swift | 42 ++++++++++ .../Events/Highlight/HighlightPostView.swift | 77 ------------------ damus/Views/PostView.swift | 81 +++++++++++++------ damusTests/ReplyTests.swift | 6 +- damusTests/damusTests.swift | 4 +- 12 files changed, 175 insertions(+), 126 deletions(-) create mode 100644 damus/Views/Events/Highlight/HighlightDraftContentView.swift delete mode 100644 damus/Views/Events/Highlight/HighlightPostView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index d4f77090..2853acea 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -399,7 +399,7 @@ 5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */; }; 5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */; }; 5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; }; - 5C4D9EA72C042FA5005EA0F7 /* HighlightPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */; }; + 5C4D9EA72C042FA5005EA0F7 /* HighlightDraftContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4D9EA62C042FA5005EA0F7 /* HighlightDraftContentView.swift */; }; 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; }; 5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; }; 5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; }; @@ -1344,7 +1344,7 @@ 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayAdminDetail.swift; sourceTree = ""; }; 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayNipList.swift; sourceTree = ""; }; 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = ""; }; - 5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightPostView.swift; sourceTree = ""; }; + 5C4D9EA62C042FA5005EA0F7 /* HighlightDraftContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDraftContentView.swift; sourceTree = ""; }; 5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = ""; }; 5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = ""; }; 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = ""; }; @@ -2728,7 +2728,7 @@ 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */, 5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */, 5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */, - 5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */, + 5C4D9EA62C042FA5005EA0F7 /* HighlightDraftContentView.swift */, ); path = Highlight; sourceTree = ""; @@ -3188,7 +3188,7 @@ 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */, 504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */, - 5C4D9EA72C042FA5005EA0F7 /* HighlightPostView.swift in Sources */, + 5C4D9EA72C042FA5005EA0F7 /* HighlightDraftContentView.swift in Sources */, 3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */, D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */, D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */, diff --git a/damus/Components/SelectableText.swift b/damus/Components/SelectableText.swift index 44c51ef5..3e9df355 100644 --- a/damus/Components/SelectableText.swift +++ b/damus/Components/SelectableText.swift @@ -59,9 +59,12 @@ struct SelectableText: View { self.highlightPostingState = newValue ? .show_post_view(highlighted_text: self.highlightPostingState.highlighted_text() ?? "") : .hide })) { if let event, case .show_post_view(let highlighted_text) = self.highlightPostingState { - HighlightPostView(damus_state: damus_state, event: event, selectedText: .constant(highlighted_text)) - .presentationDragIndicator(.visible) - .presentationDetents([.height(selectedTextHeight + 150), .medium, .large]) + PostView( + action: .highlighting(.init(selected_text: highlighted_text, source: .event(event))), + damus_state: damus_state + ) + .presentationDragIndicator(.visible) + .presentationDetents([.height(selectedTextHeight + 450), .medium, .large]) } } .frame(height: selectedTextHeight) diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 15eaa258..7b862f30 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -1051,7 +1051,7 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev //let post = tup.0 //let to_relays = tup.1 print("post \(post.content)") - guard let new_ev = post_to_event(post: post, keypair: keypair) else { + guard let new_ev = post.to_event(keypair: keypair) else { return false } postbox.send(new_ev) diff --git a/damus/Models/DraftsModel.swift b/damus/Models/DraftsModel.swift index ab71d839..56d0fcac 100644 --- a/damus/Models/DraftsModel.swift +++ b/damus/Models/DraftsModel.swift @@ -28,4 +28,5 @@ class Drafts: ObservableObject { @Published var post: DraftArtifacts? = nil @Published var replies: [NostrEvent: DraftArtifacts] = [:] @Published var quotes: [NostrEvent: DraftArtifacts] = [:] + @Published var highlights: [HighlightSource: DraftArtifacts] = [:] } diff --git a/damus/Models/HighlightEvent.swift b/damus/Models/HighlightEvent.swift index 40bd0100..024c7273 100644 --- a/damus/Models/HighlightEvent.swift +++ b/damus/Models/HighlightEvent.swift @@ -32,3 +32,31 @@ struct HighlightEvent { return highlight } } + +struct HighlightContentDraft: Hashable { + let selected_text: String + let source: HighlightSource +} + +enum HighlightSource: Hashable { + case event(NostrEvent) + case external_url(URL) + + func tags() -> [[String]] { + switch self { + case .event(let event): + return [ ["e", "\(event.id)"] ] + case .external_url(let url): + return [ ["r", "\(url)"] ] + } + } + + func ref() -> RefId { + switch self { + case .event(let event): + return .event(event.id) + case .external_url(let url): + return .reference(url.absoluteString) + } + } +} diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift index 0c229fcf..2f0785f1 100644 --- a/damus/Models/Mentions.swift +++ b/damus/Models/Mentions.swift @@ -290,12 +290,3 @@ func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags { return PostTags(blocks: post_blocks, tags: new_tags) } - -func post_to_event(post: NostrPost, keypair: FullKeypair) -> NostrEvent? { - let post_blocks = parse_post_blocks(content: post.content) - let post_tags = make_post_tags(post_blocks: post_blocks, tags: post.tags) - let content = post_tags.blocks - .map(\.asString) - .joined(separator: "") - return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: post.kind.rawValue, tags: post_tags.tags) -} diff --git a/damus/Models/Post.swift b/damus/Models/Post.swift index 9a3f2a69..6a222e93 100644 --- a/damus/Models/Post.swift +++ b/damus/Models/Post.swift @@ -17,10 +17,40 @@ struct NostrPost { self.kind = kind self.tags = tags } + + func to_event(keypair: FullKeypair) -> NostrEvent? { + let post_blocks = self.parse_blocks() + let post_tags = make_post_tags(post_blocks: post_blocks, tags: self.tags) + let content = post_tags.blocks + .map(\.asString) + .joined(separator: "") + + if self.kind == .highlight { + var new_tags = post_tags.tags.filter({ $0[safe: 0] != "comment" }) + if content.count > 0 { + new_tags.append(["comment", content]) + } + return NostrEvent(content: self.content, keypair: keypair.to_keypair(), kind: self.kind.rawValue, tags: new_tags) + } + + return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: self.kind.rawValue, tags: post_tags.tags) + } + + func parse_blocks() -> [Block] { + guard let content_for_parsing = self.default_content_for_block_parsing() else { return [] } + return parse_post_blocks(content: content_for_parsing) + } + + private func default_content_for_block_parsing() -> String? { + switch kind { + case .highlight: + return tags.filter({ $0[safe: 0] == "comment" }).first?[safe: 1] + default: + return self.content + } + } } - -/// Return a list of tags func parse_post_blocks(content: String) -> [Block] { return parse_note_content(content: .content(content, nil)).blocks } diff --git a/damus/Views/Events/Highlight/HighlightDraftContentView.swift b/damus/Views/Events/Highlight/HighlightDraftContentView.swift new file mode 100644 index 00000000..9f03f309 --- /dev/null +++ b/damus/Views/Events/Highlight/HighlightDraftContentView.swift @@ -0,0 +1,42 @@ +// +// HighlightDraftContentView.swift +// damus +// +// Created by eric on 5/26/24. +// + +import SwiftUI + +struct HighlightDraftContentView: View { + let draft: HighlightContentDraft + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack { + var attributedString: AttributedString { + var attributedString = AttributedString(draft.selected_text) + + if let range = attributedString.range(of: draft.selected_text) { + attributedString[range].backgroundColor = DamusColors.highlight + } + + return attributedString + } + + Text(attributedString) + .lineSpacing(5) + .padding(10) + } + .overlay( + RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4), + alignment: .leading + ) + + if case .external_url(let url) = draft.source { + LinkViewRepresentable(meta: .url(url)) + .frame(height: 50) + + } + } + } +} diff --git a/damus/Views/Events/Highlight/HighlightPostView.swift b/damus/Views/Events/Highlight/HighlightPostView.swift deleted file mode 100644 index d66e339a..00000000 --- a/damus/Views/Events/Highlight/HighlightPostView.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// HighlightPostView.swift -// damus -// -// Created by eric on 5/26/24. -// - -import SwiftUI - -struct HighlightPostView: View { - let damus_state: DamusState - let event: NostrEvent - @Binding var selectedText: String - - @Environment(\.dismiss) var dismiss - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - VStack { - HStack(spacing: 5.0) { - Button(action: { - dismiss() - }, label: { - Text("Cancel", comment: "Button to cancel out of highlighting a note.") - .padding(10) - }) - .buttonStyle(NeutralButtonStyle()) - - Spacer() - - Button(NSLocalizedString("Post", comment: "Button to post a highlight.")) { - let tags: [[String]] = [ ["e", "\(self.event.id)"] ] - - let kind = NostrKind.highlight.rawValue - guard let ev = NostrEvent(content: selectedText, keypair: damus_state.keypair, kind: kind, tags: tags) else { - return - } - damus_state.postbox.send(ev) - dismiss() - } - .bold() - .buttonStyle(GradientButtonStyle(padding: 10)) - } - - Divider() - .foregroundColor(DamusColors.neutral3) - .padding(.top, 5) - } - .frame(height: 30) - .padding() - .padding(.top, 15) - - HStack { - var attributedString: AttributedString { - var attributedString = AttributedString(selectedText) - - if let range = attributedString.range(of: selectedText) { - attributedString[range].backgroundColor = DamusColors.highlight - } - - return attributedString - } - - Text(attributedString) - .lineSpacing(5) - .padding(10) - } - .overlay( - RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4), - alignment: .leading - ) - .padding() - - Spacer() - } - } -} diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift index 2f8c3e72..23b8a4ea 100644 --- a/damus/Views/PostView.swift +++ b/damus/Views/PostView.swift @@ -30,15 +30,18 @@ enum PostAction { case replying_to(NostrEvent) case quoting(NostrEvent) case posting(PostTarget) + case highlighting(HighlightContentDraft) var ev: NostrEvent? { switch self { - case .replying_to(let ev): - return ev - case .quoting(let ev): - return ev - case .posting: - return nil + case .replying_to(let ev): + return ev + case .quoting(let ev): + return ev + case .posting: + return nil + case .highlighting: + return nil } } } @@ -128,7 +131,12 @@ struct PostView: View { } var posting_disabled: Bool { - return is_post_empty || uploading_disabled + switch action { + case .highlighting(_): + return false + default: + return is_post_empty || uploading_disabled + } } // Returns a valid height for the text box, even when textHeight is not a number @@ -204,6 +212,8 @@ struct PostView: View { damus_state.drafts.quotes.removeValue(forKey: quoting) case .posting: damus_state.drafts.post = nil + case .highlighting(let draft): + damus_state.drafts.highlights.removeValue(forKey: draft.source) } } @@ -371,6 +381,9 @@ struct PostView: View { if case .quoting(let ev) = action { BuilderEventView(damus: damus_state, event: ev) } + else if case .highlighting(let draft) = action { + HighlightDraftContentView(draft: draft) + } } .padding(.horizontal) } @@ -454,14 +467,15 @@ struct PostView: View { let loaded_draft = load_draft() switch action { - case .replying_to(let replying_to): - references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to) - case .quoting(let quoting): - references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting) - case .posting(let target): - guard !loaded_draft else { break } - - fill_target_content(target: target) + case .replying_to(let replying_to): + references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to) + case .quoting(let quoting): + references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting) + case .posting(let target): + guard !loaded_draft else { break } + fill_target_content(target: target) + case .highlighting(let draft): + references = [draft.source.ref()] } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -597,6 +611,8 @@ func set_draft_for_post(drafts: Drafts, action: PostAction, artifacts: DraftArti drafts.quotes[ev] = artifacts case .posting: drafts.post = artifacts + case .highlighting(let draft): + drafts.highlights[draft.source] = artifacts } } @@ -608,6 +624,8 @@ func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts? return drafts.quotes[ev] case .posting: return drafts.post + case .highlighting(let draft): + return drafts.highlights[draft.source] } } @@ -669,18 +687,20 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post var tags: [[String]] = [] switch action { - case .replying_to(let replying_to): - // start off with the reply tags - tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair) + case .replying_to(let replying_to): + // start off with the reply tags + tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair) - case .quoting(let ev): - content.append(" nostr:" + bech32_note_id(ev.id)) + case .quoting(let ev): + content.append(" nostr:" + bech32_note_id(ev.id)) - if let quoted_ev = state.events.lookup(ev.id) { - tags.append(["p", quoted_ev.pubkey.hex()]) - } - case .posting(let postTarget): - break + if let quoted_ev = state.events.lookup(ev.id) { + tags.append(["p", quoted_ev.pubkey.hex()]) + } + case .posting(let postTarget): + break + case .highlighting(let draft): + break } // include pubkeys @@ -690,6 +710,17 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post // append additional tags tags += uploadedMedias.compactMap { $0.metadata?.to_tag() } + + switch action { + case .highlighting(let draft): + tags.append(contentsOf: draft.source.tags()) + if !(content.isEmpty || content.allSatisfy { $0.isWhitespace }) { + tags.append(["comment", content]) + } + return NostrPost(content: draft.selected_text, kind: .highlight, tags: tags) + default: + break + } return NostrPost(content: content, kind: .text, tags: tags) } diff --git a/damusTests/ReplyTests.swift b/damusTests/ReplyTests.swift index fda143eb..4395a673 100644 --- a/damusTests/ReplyTests.swift +++ b/damusTests/ReplyTests.swift @@ -240,7 +240,7 @@ class ReplyTests: XCTestCase { let content = "this is a @\(pk.npub) mention" let blocks = parse_post_blocks(content: content) let post = NostrPost(content: content, tags: [["e", evid.hex()]]) - let ev = post_to_event(post: post, keypair: test_keypair_full)! + let ev = post.to_event(keypair: test_keypair_full)! XCTAssertEqual(ev.tags.count, 2) XCTAssertEqual(blocks.count, 3) @@ -255,7 +255,7 @@ class ReplyTests: XCTestCase { let content = "this is a @\(nsec) mention" let blocks = parse_post_blocks(content: content) let post = NostrPost(content: content, tags: [["e", evid.hex()]]) - let ev = post_to_event(post: post, keypair: test_keypair_full)! + let ev = post.to_event(keypair: test_keypair_full)! XCTAssertEqual(ev.tags.count, 2) XCTAssertEqual(blocks.count, 3) @@ -275,7 +275,7 @@ class ReplyTests: XCTestCase { ] let post = NostrPost(content: "this is a (@\(pubkey.npub)) mention", tags: tags) - let ev = post_to_event(post: post, keypair: test_keypair_full)! + let ev = post.to_event(keypair: test_keypair_full)! XCTAssertEqual(ev.content, "this is a (nostr:\(pubkey.npub)) mention") XCTAssertEqual(ev.tags[2][1].string(), pubkey.description) diff --git a/damusTests/damusTests.swift b/damusTests/damusTests.swift index 55b1374d..71fd1df4 100644 --- a/damusTests/damusTests.swift +++ b/damusTests/damusTests.swift @@ -193,7 +193,7 @@ class damusTests: XCTestCase { func testMakeHashtagPost() { let post = NostrPost(content: "#damus some content #bitcoin derp #かっこいい wow", tags: []) - let ev = post_to_event(post: post, keypair: test_keypair_full)! + let ev = post.to_event(keypair: test_keypair_full)! XCTAssertEqual(ev.tags.count, 3) XCTAssertEqual(ev.content, "#damus some content #bitcoin derp #かっこいい wow") @@ -270,7 +270,7 @@ class damusTests: XCTestCase { private func createEventFromContentString(_ content: String) -> NostrEvent { let post = NostrPost(content: content, tags: []) - guard let ev = post_to_event(post: post, keypair: test_keypair_full) else { + guard let ev = post.to_event(keypair: test_keypair_full) else { XCTFail("Could not create event") return test_note }