[PATCH damus v3 2/2] ux: Create Highlights

5 views
Skip to first unread message

ericholguin

unread,
May 27, 2024, 3:07:22 PMMay 27
to pat...@damus.io, dan...@daquino.me, ericholguin
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 <erich...@apache.org>
---
damus.xcodeproj/project.pbxproj | 6 +-
damus/Components/SelectableText.swift | 60 ++++++++++++--
damus/Components/TranslateView.swift | 2 +-
.../Events/Highlight/HighlightPostView.swift | 78 +++++++++++++++++++
.../Views/Events/Longform/LongformView.swift | 2 +-
damus/Views/NoteContentView.swift | 4 +-
damus/Views/Profile/AboutView.swift | 6 +-
7 files changed, 144 insertions(+), 14 deletions(-)
create mode 100644 damus/Views/Events/Highlight/HighlightPostView.swift

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
index 439cb295..3748b562 100644
--- a/damus.xcodeproj/project.pbxproj
+++ b/damus.xcodeproj/project.pbxproj
@@ -398,6 +398,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 */; };
@@ -1327,6 +1328,7 @@
5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayAdminDetail.swift; sourceTree = "<group>"; };
5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayNipList.swift; sourceTree = "<group>"; };
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
+ 5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightPostView.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
@@ -1659,7 +1661,6 @@
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */,
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
B533694D2B66D791008A805E /* MutelistManager.swift */,
- D7C28E3A2BBB4D0000EE459F /* VideoCache.swift */,
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
);
path = Models;
@@ -2562,7 +2563,6 @@
E06336A92B75832100A88E6B /* ImageMetadataTest.swift */,
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */,
D72927AC2BAB515C00F93E90 /* RelayURLTests.swift */,
- D7831AF72BBE11E2005DA780 /* VideoCacheTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -2689,6 +2689,7 @@
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */,
5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */,
5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */,
+ 5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */,
);
path = Highlight;
sourceTree = "<group>";
@@ -3128,6 +3129,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..fbee838f 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<Bool>, selectedText: Binding<String>) {
+ 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<Self>) -> UITextView {
- let view = UITextView()
+ func makeUIView(context: UIViewRepresentableContext<Self>) -> 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<Self>) {
+ func updateUIView(_ uiView: TextView, context: UIViewRepresentableContext<Self>) {
let mutableAttributedString = createNSAttributedString()
uiView.attributedText = mutableAttributedString
uiView.textAlignment = self.textAlignment
diff --git a/damus/Components/TranslateView.swift b/damus/Components/TranslateView.swift
index c4fa8f7b..b1936b97 100644
--- a/damus/Components/TranslateView.swift
+++ b/damus/Components/TranslateView.swift
@@ -53,7 +53,7 @@ struct TranslateView: View {
.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..87ff1a5e
--- /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..9e12000b 100644
--- a/damus/Views/Events/Longform/LongformView.swift
+++ b/damus/Views/Events/Longform/LongformView.swift
@@ -24,7 +24,7 @@ struct LongformView: View {

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 491fe4ac..c85ac64b 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 {
--
2.39.3 (Apple Git-145)

Daniel D'Aquino

unread,
Jun 21, 2024, 5:43:57 PM (14 days ago) Jun 21
to ericholguin, sembene_truestar via patches, William Casarin
Thanks Eric, and sorry for the delay!

I reviewed all the changes and they look good to me! I just had to fix a few merge conflicts to rebase this on top of the current `master` branch, and made two very minor nitpick changes (on separate commits).

I sent it to Will in case he wants to double-check anything (https://github.com/damus-io/damus/pull/2307), but I think we should be good to merge this today!
Reply all
Reply to author
Forward
0 new messages