From 0dce7aea4561574b295cb25ef57e84d2bb531da6 Mon Sep 17 00:00:00 2001 From: ericholguin Date: Mon, 27 May 2024 13:06:00 -0600 Subject: [PATCH] ux: Create Highlights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch allows users to create a highlight in Damus. This is done by modifying the menu options when text is selected, including a custom highlight option. This option presents a sheet to the user of what they are highlighting with a cancel or post button. If they press Post the sheet will dismiss and their highlight will be posted. Testing —— iPhone 15 Pro Max (17.3.1) Dark Mode: https://v.nostr.build/wGDnx.mp4 iPhone SE (3rd generation) (16.4) Light Mode: https://v.nostr.build/xEK0e.mp4 —— Changelog-Added: Ability to create highlights Signed-off-by: ericholguin --- damus.xcodeproj/project.pbxproj | 4 + damus/Components/SelectableText.swift | 64 ++++++++++++--- damus/Components/TranslateView.swift | 4 +- .../Events/Highlight/HighlightPostView.swift | 78 +++++++++++++++++++ .../Views/Events/Longform/LongformView.swift | 4 +- damus/Views/NoteContentView.swift | 4 +- damus/Views/Profile/AboutView.swift | 6 +- 7 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 damus/Views/Events/Highlight/HighlightPostView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index fae18171..e628c7b0 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -399,6 +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 */; }; 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 */; }; @@ -1339,6 +1340,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 = ""; }; 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 = ""; }; @@ -2719,6 +2721,7 @@ 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */, 5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */, 5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */, + 5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */, ); path = Highlight; sourceTree = ""; @@ -3177,6 +3180,7 @@ 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */, 504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */, + 5C4D9EA72C042FA5005EA0F7 /* HighlightPostView.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 975d5810..05500eb9 100644 --- a/damus/Components/SelectableText.swift +++ b/damus/Components/SelectableText.swift @@ -9,16 +9,20 @@ import UIKit import SwiftUI struct SelectableText: View { - + let damus_state: DamusState + let event: NostrEvent let attributedString: AttributedString let textAlignment: NSTextAlignment - + @State private var showHighlightPost = false + @State private var selectedText = "" @State private var selectedTextHeight: CGFloat = .zero @State private var selectedTextWidth: CGFloat = .zero - + let size: EventViewKind - - init(attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) { + + init(damus_state: DamusState, event: NostrEvent, attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) { + self.damus_state = damus_state + self.event = event self.attributedString = attributedString self.textAlignment = textAlignment ?? NSTextAlignment.natural self.size = size @@ -32,6 +36,8 @@ struct SelectableText: View { font: eventviewsize_to_uifont(size), fixedWidth: selectedTextWidth, textAlignment: self.textAlignment, + showHighlightPost: $showHighlightPost, + selectedText: $selectedText, height: $selectedTextHeight ) .padding([.leading, .trailing], -1.0) @@ -46,10 +52,44 @@ struct SelectableText: View { self.selectedTextWidth = newSize.width } } + .sheet(isPresented: $showHighlightPost) { + HighlightPostView(damus_state: damus_state, event: event, selectedText: $selectedText) + .presentationDragIndicator(.visible) + .presentationDetents([.height(selectedTextHeight + 150), .medium, .large]) + } .frame(height: selectedTextHeight) } } +fileprivate class TextView: UITextView { + @Binding var showHighlightPost: Bool + @Binding var selectedText: String + + init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: Binding, selectedText: Binding) { + self._showHighlightPost = showHighlightPost + self._selectedText = selectedText + super.init(frame: frame, textContainer: textContainer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + if action == #selector(highlightText(_:)) { + return true + } + return super.canPerformAction(action, withSender: sender) + } + + @objc public func highlightText(_ sender: Any?) { + guard let selectedRange = self.selectedTextRange else { return } + selectedText = self.text(in: selectedRange) ?? "" + showHighlightPost.toggle() + } + +} + fileprivate struct TextViewRepresentable: UIViewRepresentable { let attributedString: AttributedString @@ -57,11 +97,12 @@ struct SelectableText: View { let font: UIFont let fixedWidth: CGFloat let textAlignment: NSTextAlignment - + @Binding var showHighlightPost: Bool + @Binding var selectedText: String @Binding var height: CGFloat - func makeUIView(context: UIViewRepresentableContext) -> UITextView { - let view = UITextView() + func makeUIView(context: UIViewRepresentableContext) -> TextView { + let view = TextView(frame: .zero, textContainer: nil, showHighlightPost: $showHighlightPost, selectedText: $selectedText) view.isEditable = false view.dataDetectorTypes = .all view.isSelectable = true @@ -71,10 +112,15 @@ struct SelectableText: View { view.textContainerInset.left = 1.0 view.textContainerInset.right = 1.0 view.textAlignment = textAlignment + + let menuController = UIMenuController.shared + let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:))) + menuController.menuItems = [highlightItem] + return view } - func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { + func updateUIView(_ uiView: TextView, context: UIViewRepresentableContext) { let mutableAttributedString = createNSAttributedString() uiView.attributedText = mutableAttributedString uiView.textAlignment = self.textAlignment diff --git a/damus/Components/TranslateView.swift b/damus/Components/TranslateView.swift index c4fa8f7b..2f34cdc3 100644 --- a/damus/Components/TranslateView.swift +++ b/damus/Components/TranslateView.swift @@ -51,9 +51,9 @@ struct TranslateView: View { .foregroundColor(.gray) .font(.footnote) .padding([.top, .bottom], 10) - + if self.size == .selected { - SelectableText(attributedString: artifacts.content.attributed, size: self.size) + SelectableText(damus_state: damus_state, event: event, attributedString: artifacts.content.attributed, size: self.size) } else { artifacts.content.text .font(eventviewsize_to_font(self.size, font_size: font_size)) diff --git a/damus/Views/Events/Highlight/HighlightPostView.swift b/damus/Views/Events/Highlight/HighlightPostView.swift new file mode 100644 index 00000000..d0a23f50 --- /dev/null +++ b/damus/Views/Events/Highlight/HighlightPostView.swift @@ -0,0 +1,78 @@ +// +// 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.")) { + var tags: [[String]] = [ ["e", "\(self.event.id)"] ] + tags.append(["context", self.event.content]) + + 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(self.event.content) + + 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/Events/Longform/LongformView.swift b/damus/Views/Events/Longform/LongformView.swift index 639371e2..0b7e7203 100644 --- a/damus/Views/Events/Longform/LongformView.swift +++ b/damus/Views/Events/Longform/LongformView.swift @@ -21,10 +21,10 @@ struct LongformView: View { var options: EventViewOptions { return [.wide, .no_mentions, .no_replying_to] } - + var body: some View { EventShell(state: state, event: event.event, options: options) { - SelectableText(attributedString: AttributedString(stringLiteral: event.title ?? "Untitled"), size: .title) + SelectableText(damus_state: state, event: event.event, attributedString: AttributedString(stringLiteral: event.title ?? "Untitled"), size: .title) NoteContentView(damus_state: state, event: event.event, blur_images: false, size: .selected, options: options) } diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index 45b320ed..29a44258 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -132,10 +132,10 @@ struct NoteContentView: View { VStack(alignment: .leading) { if size == .selected { if with_padding { - SelectableText(attributedString: artifacts.content.attributed, size: self.size) + SelectableText(damus_state: damus_state, event: self.event, attributedString: artifacts.content.attributed, size: self.size) .padding(.horizontal) } else { - SelectableText(attributedString: artifacts.content.attributed, size: self.size) + SelectableText(damus_state: damus_state, event: self.event, attributedString: artifacts.content.attributed, size: self.size) } } else { if with_padding { diff --git a/damus/Views/Profile/AboutView.swift b/damus/Views/Profile/AboutView.swift index 905b8001..892d23f0 100644 --- a/damus/Views/Profile/AboutView.swift +++ b/damus/Views/Profile/AboutView.swift @@ -26,7 +26,11 @@ struct AboutView: View { Group { if let about_string { let truncated_about = show_full_about ? about_string : about_string.truncateOrNil(maxLength: max_about_length) - SelectableText(attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline) + SelectableText(damus_state: state, event: NostrEvent( + content: "", + keypair: jack_keypair, + createdAt: UInt32(Date().timeIntervalSince1970 - 100) + )!, attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline) if truncated_about != nil { if show_full_about {