ux: Create Highlights
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 <ericholguin@apache.org>
This commit is contained in:
committed by
Daniel D’Aquino
parent
6376c61bad
commit
0dce7aea45
@@ -399,6 +399,7 @@
|
|||||||
5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */; };
|
5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */; };
|
||||||
5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */; };
|
5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */; };
|
||||||
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.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 */; };
|
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
|
||||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
||||||
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.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 = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
|
||||||
@@ -2719,6 +2721,7 @@
|
|||||||
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */,
|
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */,
|
||||||
5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */,
|
5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */,
|
||||||
5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */,
|
5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */,
|
||||||
|
5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */,
|
||||||
);
|
);
|
||||||
path = Highlight;
|
path = Highlight;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3177,6 +3180,7 @@
|
|||||||
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
||||||
D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */,
|
D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */,
|
||||||
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */,
|
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */,
|
||||||
|
5C4D9EA72C042FA5005EA0F7 /* HighlightPostView.swift in Sources */,
|
||||||
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
|
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
|
||||||
D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */,
|
D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */,
|
||||||
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */,
|
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */,
|
||||||
|
|||||||
@@ -9,16 +9,20 @@ import UIKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SelectableText: View {
|
struct SelectableText: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
let event: NostrEvent
|
||||||
let attributedString: AttributedString
|
let attributedString: AttributedString
|
||||||
let textAlignment: NSTextAlignment
|
let textAlignment: NSTextAlignment
|
||||||
|
@State private var showHighlightPost = false
|
||||||
|
@State private var selectedText = ""
|
||||||
@State private var selectedTextHeight: CGFloat = .zero
|
@State private var selectedTextHeight: CGFloat = .zero
|
||||||
@State private var selectedTextWidth: CGFloat = .zero
|
@State private var selectedTextWidth: CGFloat = .zero
|
||||||
|
|
||||||
let size: EventViewKind
|
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.attributedString = attributedString
|
||||||
self.textAlignment = textAlignment ?? NSTextAlignment.natural
|
self.textAlignment = textAlignment ?? NSTextAlignment.natural
|
||||||
self.size = size
|
self.size = size
|
||||||
@@ -32,6 +36,8 @@ struct SelectableText: View {
|
|||||||
font: eventviewsize_to_uifont(size),
|
font: eventviewsize_to_uifont(size),
|
||||||
fixedWidth: selectedTextWidth,
|
fixedWidth: selectedTextWidth,
|
||||||
textAlignment: self.textAlignment,
|
textAlignment: self.textAlignment,
|
||||||
|
showHighlightPost: $showHighlightPost,
|
||||||
|
selectedText: $selectedText,
|
||||||
height: $selectedTextHeight
|
height: $selectedTextHeight
|
||||||
)
|
)
|
||||||
.padding([.leading, .trailing], -1.0)
|
.padding([.leading, .trailing], -1.0)
|
||||||
@@ -46,10 +52,44 @@ struct SelectableText: View {
|
|||||||
self.selectedTextWidth = newSize.width
|
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)
|
.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 {
|
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||||
|
|
||||||
let attributedString: AttributedString
|
let attributedString: AttributedString
|
||||||
@@ -57,11 +97,12 @@ struct SelectableText: View {
|
|||||||
let font: UIFont
|
let font: UIFont
|
||||||
let fixedWidth: CGFloat
|
let fixedWidth: CGFloat
|
||||||
let textAlignment: NSTextAlignment
|
let textAlignment: NSTextAlignment
|
||||||
|
@Binding var showHighlightPost: Bool
|
||||||
|
@Binding var selectedText: String
|
||||||
@Binding var height: CGFloat
|
@Binding var height: CGFloat
|
||||||
|
|
||||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
|
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
|
||||||
let view = UITextView()
|
let view = TextView(frame: .zero, textContainer: nil, showHighlightPost: $showHighlightPost, selectedText: $selectedText)
|
||||||
view.isEditable = false
|
view.isEditable = false
|
||||||
view.dataDetectorTypes = .all
|
view.dataDetectorTypes = .all
|
||||||
view.isSelectable = true
|
view.isSelectable = true
|
||||||
@@ -71,10 +112,15 @@ struct SelectableText: View {
|
|||||||
view.textContainerInset.left = 1.0
|
view.textContainerInset.left = 1.0
|
||||||
view.textContainerInset.right = 1.0
|
view.textContainerInset.right = 1.0
|
||||||
view.textAlignment = textAlignment
|
view.textAlignment = textAlignment
|
||||||
|
|
||||||
|
let menuController = UIMenuController.shared
|
||||||
|
let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
|
||||||
|
menuController.menuItems = [highlightItem]
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
|
func updateUIView(_ uiView: TextView, context: UIViewRepresentableContext<Self>) {
|
||||||
let mutableAttributedString = createNSAttributedString()
|
let mutableAttributedString = createNSAttributedString()
|
||||||
uiView.attributedText = mutableAttributedString
|
uiView.attributedText = mutableAttributedString
|
||||||
uiView.textAlignment = self.textAlignment
|
uiView.textAlignment = self.textAlignment
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ struct TranslateView: View {
|
|||||||
.padding([.top, .bottom], 10)
|
.padding([.top, .bottom], 10)
|
||||||
|
|
||||||
if self.size == .selected {
|
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 {
|
} else {
|
||||||
artifacts.content.text
|
artifacts.content.text
|
||||||
.font(eventviewsize_to_font(self.size, font_size: font_size))
|
.font(eventviewsize_to_font(self.size, font_size: font_size))
|
||||||
|
|||||||
78
damus/Views/Events/Highlight/HighlightPostView.swift
Normal file
78
damus/Views/Events/Highlight/HighlightPostView.swift
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ struct LongformView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
EventShell(state: state, event: event.event, options: options) {
|
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)
|
NoteContentView(damus_state: state, event: event.event, blur_images: false, size: .selected, options: options)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,10 +132,10 @@ struct NoteContentView: View {
|
|||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if size == .selected {
|
if size == .selected {
|
||||||
if with_padding {
|
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)
|
.padding(.horizontal)
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
if with_padding {
|
if with_padding {
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ struct AboutView: View {
|
|||||||
Group {
|
Group {
|
||||||
if let about_string {
|
if let about_string {
|
||||||
let truncated_about = show_full_about ? about_string : about_string.truncateOrNil(maxLength: max_about_length)
|
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 truncated_about != nil {
|
||||||
if show_full_about {
|
if show_full_about {
|
||||||
|
|||||||
Reference in New Issue
Block a user