Merge pull request #2307 from damus-io/review_highlights_2024-06-19_rebased

Highlights (rebased to solve merge conflicts + minor tweaks)
This commit is contained in:
Daniel D’Aquino
2024-06-24 11:30:59 -07:00
committed by GitHub
24 changed files with 724 additions and 26 deletions
+33
View File
@@ -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 */; };
@@ -406,6 +407,11 @@
5C7389B12B6EFA7100781E0A /* ProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B02B6EFA7100781E0A /* ProxyView.swift */; };
5C7389B72B9E692E00781E0A /* MutinyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B62B9E692E00781E0A /* MutinyButton.swift */; };
5C7389B92B9E69ED00781E0A /* MutinyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */; };
5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */; };
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */; };
5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */; };
5CC852A42BDF3CA10039FFC5 /* HighlightLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */; };
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */; };
5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */; };
5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */; };
5CF2DCCE2AABE1A500984B8D /* DamusLightGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */; };
@@ -1334,6 +1340,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>"; };
@@ -1341,6 +1348,11 @@
5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = "<group>"; };
5C7389B62B9E692E00781E0A /* MutinyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyButton.swift; sourceTree = "<group>"; };
5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyGradient.swift; sourceTree = "<group>"; };
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEvent.swift; sourceTree = "<group>"; };
5CC8529E2BD744F60039FFC5 /* HighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightView.swift; sourceTree = "<group>"; };
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDescription.swift; sourceTree = "<group>"; };
5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightLink.swift; sourceTree = "<group>"; };
5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEventRef.swift; sourceTree = "<group>"; };
5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeutralButtonStyle.swift; sourceTree = "<group>"; };
5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicView.swift; sourceTree = "<group>"; };
5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLightGradient.swift; sourceTree = "<group>"; };
@@ -1668,6 +1680,8 @@
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
B533694D2B66D791008A805E /* MutelistManager.swift */,
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */,
D7C28E3A2BBB4D0000EE459F /* VideoCache.swift */,
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -2402,6 +2416,7 @@
4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup;
children = (
5CC852A02BDED9970039FFC5 /* Highlight */,
4CA927682A290F8F0098A105 /* Components */,
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
@@ -2699,6 +2714,18 @@
path = Images;
sourceTree = "<group>";
};
5CC852A02BDED9970039FFC5 /* Highlight */ = {
isa = PBXGroup;
children = (
5CC8529E2BD744F60039FFC5 /* HighlightView.swift */,
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */,
5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */,
5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */,
5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */,
);
path = Highlight;
sourceTree = "<group>";
};
7C0F392D29B57C8F0039859C /* Extensions */ = {
isa = PBXGroup;
children = (
@@ -3153,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 */,
@@ -3183,6 +3211,7 @@
4C32B94D2A9AD44700DC3548 /* Offset.swift in Sources */,
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
5CC852A42BDF3CA10039FFC5 /* HighlightLink.swift in Sources */,
4C32B9552A9AD44700DC3548 /* ByteBuffer.swift in Sources */,
4C32B95B2A9AD44700DC3548 /* NativeObject.swift in Sources */,
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */,
@@ -3322,6 +3351,7 @@
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */,
4C94D6432BA5AEFE00C26EFF /* QuoteRepostsView.swift in Sources */,
D7EDED332B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */,
4CA352AE2A76C1AC003BB08B /* FollowedNotify.swift in Sources */,
@@ -3365,7 +3395,9 @@
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */,
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */,
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */,
5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */,
4C9146FD2A2A87C200DDEA40 /* wasm.c in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
@@ -3477,6 +3509,7 @@
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */,
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF2",
"green" : "0xD8",
"red" : "0xF4"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x45",
"green" : "0x17",
"red" : "0x47"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
+1
View File
@@ -28,6 +28,7 @@ class DamusColors {
static let green = Color("DamusGreen")
static let purple = Color("DamusPurple")
static let deepPurple = Color("DamusDeepPurple")
static let highlight = Color("DamusHighlight")
static let blue = Color("DamusBlue")
static let bitcoin = Color("Bitcoin")
static let success = Color("DamusSuccessPrimary")
+61 -7
View File
@@ -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,9 @@ struct SelectableText: View {
font: eventviewsize_to_uifont(size),
fixedWidth: selectedTextWidth,
textAlignment: self.textAlignment,
enableHighlighting: self.enableHighlighting(),
showHighlightPost: $showHighlightPost,
selectedText: $selectedText,
height: $selectedTextHeight
)
.padding([.leading, .trailing], -1.0)
@@ -46,8 +53,48 @@ struct SelectableText: View {
self.selectedTextWidth = newSize.width
}
}
.sheet(isPresented: $showHighlightPost) {
if let event {
HighlightPostView(damus_state: damus_state, event: event, selectedText: $selectedText)
.presentationDragIndicator(.visible)
.presentationDetents([.height(selectedTextHeight + 150), .medium, .large])
}
}
.frame(height: selectedTextHeight)
}
func enableHighlighting() -> Bool {
self.event != nil
}
}
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 {
@@ -57,11 +104,13 @@ struct SelectableText: View {
let font: UIFont
let fixedWidth: CGFloat
let textAlignment: NSTextAlignment
let enableHighlighting: Bool
@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 +120,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 = self.enableHighlighting ? [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
+1 -1
View File
@@ -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))
+34
View File
@@ -0,0 +1,34 @@
//
// HighlightEvent.swift
// damus
//
// Created by eric on 4/22/24.
//
import Foundation
struct HighlightEvent {
let event: NostrEvent
var event_ref: String? = nil
var url_ref: URL? = nil
var context: String? = nil
static func parse(from ev: NostrEvent) -> HighlightEvent {
var highlight = HighlightEvent(event: ev)
for tag in ev.tags {
guard tag.count >= 2 else { continue }
switch tag[0].string() {
case "e": highlight.event_ref = tag[1].string()
case "a": highlight.event_ref = tag[1].string()
case "r": highlight.url_ref = URL(string: tag[1].string())
case "context": highlight.context = tag[1].string()
default:
break
}
}
return highlight
}
}
+2 -2
View File
@@ -184,7 +184,7 @@ class HomeModel: ContactsDelegate {
}
switch kind {
case .chat, .longform, .text:
case .chat, .longform, .text, .highlight:
handle_text_event(sub_id: sub_id, ev)
case .contacts:
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
@@ -586,7 +586,7 @@ class HomeModel: ContactsDelegate {
func subscribe_to_home_filters(friends fs: [Pubkey]? = nil, relay_id: RelayURL? = nil) {
// TODO: separate likes?
var home_filter_kinds: [NostrKind] = [
.text, .longform, .boost
.text, .longform, .boost, .highlight
]
if !damus_state.settings.onlyzaps_mode {
home_filter_kinds.append(.like)
+1 -1
View File
@@ -62,7 +62,7 @@ class ProfileModel: ObservableObject, Equatable {
}
func subscribe() {
var text_filter = NostrFilter(kinds: [.text, .longform])
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
profile_filter.authors = [pubkey]
+1 -1
View File
@@ -36,7 +36,7 @@ class SearchModel: ObservableObject {
func subscribe() {
// since 1 month
search.limit = self.limit
search.kinds = [.text, .like, .longform]
search.kinds = [.text, .like, .longform, .highlight]
//likes_filter.ids = ref_events.referenced_ids!
+1
View File
@@ -22,6 +22,7 @@ enum NostrKind: UInt32, Codable {
case longform = 30023
case zap = 9735
case zap_request = 9734
case highlight = 9802
case nwc_request = 23194
case nwc_response = 23195
case http_auth = 27235
+2
View File
@@ -45,6 +45,8 @@ struct EventView: View {
}
} else if event.known_kind == .longform {
LongformPreview(state: damus, ev: event, options: options)
} else if event.known_kind == .highlight {
HighlightView(state: damus, event: event, options: options)
} else {
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
//.padding([.top], 6)
@@ -16,7 +16,15 @@ struct ReplyPart: View {
var body: some View {
Group {
if let reply_ref = event.thread_reply()?.reply {
ReplyDescription(event: event, replying_to: events.lookup(reply_ref.note_id), ndb: ndb)
let replying_to = events.lookup(reply_ref.note_id)
if event.known_kind != .highlight {
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
} else if event.known_kind == .highlight {
HighlightDescription(event: event, highlighted_event: replying_to, ndb: ndb)
}
else {
EmptyView()
}
} else {
EmptyView()
}
+2
View File
@@ -35,6 +35,8 @@ struct EventBody: View {
if !options.contains(.truncate_content) {
note_content
}
} else if event.known_kind == .highlight {
HighlightBodyView(state: damus_state, ev: event, options: options)
} else {
note_content
}
@@ -0,0 +1,53 @@
//
// HighlightDescription.swift
// damus
//
// Created by eric on 4/28/24.
//
import SwiftUI
// Modified from Reply Description
struct HighlightDescription: View {
let event: NostrEvent
let highlighted_event: NostrEvent?
let ndb: Ndb
var body: some View {
(Text(Image(systemName: "highlighter")) + Text(verbatim: " \(highlight_desc(ndb: ndb, event: event, highlighted_event: highlighted_event))"))
.font(.footnote)
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct HighlightDescription_Previews: PreviewProvider {
static var previews: some View {
HighlightDescription(event: test_note, highlighted_event: test_note, ndb: test_damus_state.ndb)
}
}
func highlight_desc(ndb: Ndb, event: NostrEvent, highlighted_event: NostrEvent?, locale: Locale = Locale.current) -> String {
let desc = make_reply_description(event, replying_to: highlighted_event)
let pubkeys = desc.pubkeys
let bundle = bundleForLocale(locale: locale)
if pubkeys.count == 0 {
return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.")
}
guard let profile_txn = NdbTxn(ndb: ndb) else {
return ""
}
let names: [String] = pubkeys.map { pk in
let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn)
return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50)
}
let uniqueNames: [String] = Array(Set(names))
return String(format: NSLocalizedString("Highlighted %@", bundle: bundle, comment: "Label to indicate that the user is highlighting 1 user."), locale: locale, uniqueNames.first ?? "")
}
@@ -0,0 +1,92 @@
//
// HighlightEventRef.swift
// damus
//
// Created by eric on 4/29/24.
//
import SwiftUI
import Kingfisher
struct HighlightEventRef: View {
let damus_state: DamusState
let event_ref: NoteId
init(damus_state: DamusState, event_ref: NoteId) {
self.damus_state = damus_state
self.event_ref = event_ref
}
struct FailedImage: View {
var body: some View {
Image("markdown")
.resizable()
.foregroundColor(DamusColors.neutral6)
.background(DamusColors.neutral3)
.frame(width: 35, height: 35)
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
.scaledToFit()
}
}
var body: some View {
EventLoaderView(damus_state: damus_state, event_id: event_ref) { event in
EventMutingContainerView(damus_state: damus_state, event: event) {
if event.known_kind == .longform {
HStack(alignment: .top, spacing: 10) {
let longform_event = LongformEvent.parse(from: event)
if let url = longform_event.image {
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note, disable_animation: true)
.image_fade(duration: 0.25)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.background {
FailedImage()
}
.frame(width: 35, height: 35)
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
.scaledToFit()
} else {
FailedImage()
}
VStack(alignment: .leading, spacing: 5) {
Text(longform_event.title ?? "Untitled")
.font(.system(size: 14, weight: .bold))
.lineLimit(1)
let profile_txn = damus_state.profiles.lookup(id: longform_event.event.pubkey, txn_name: "highlight-profile")
let profile = profile_txn?.unsafeUnownedValue
if let display_name = profile?.display_name {
Text(display_name)
.font(.system(size: 12))
.foregroundColor(.gray)
} else if let name = profile?.name {
Text(name)
.font(.system(size: 12))
.foregroundColor(.gray)
}
}
}
.padding([.leading, .vertical], 7)
.frame(maxWidth: .infinity, alignment: .leading)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral3, lineWidth: 2)
)
} else {
EmptyView()
}
}
}
}
}
@@ -0,0 +1,101 @@
//
// HighlightLink.swift
// damus
//
// Created by eric on 4/28/24.
//
import SwiftUI
import Kingfisher
struct HighlightLink: View {
let state: DamusState
let url: URL
let content: String
@Environment(\.openURL) var openURL
func text_fragment_url() -> URL? {
let fragmentDirective = "#:~:"
let textDirective = "text="
let separator = ","
var text = ""
let components = content.components(separatedBy: " ")
if components.count <= 10 {
text = content
} else {
let textStart = Array(components.prefix(5)).joined(separator: " ")
let textEnd = Array(components.suffix(2)).joined(separator: " ")
text = textStart + separator + textEnd
}
let url_with_fragments = url.absoluteString + fragmentDirective + textDirective + text
return URL(string: url_with_fragments)
}
func get_url_icon() -> URL? {
var icon = URL(string: url.absoluteString + "/favicon.ico")
if let url_host = url.host() {
icon = URL(string: "https://" + url_host + "/favicon.ico")
}
return icon
}
var body: some View {
Button(action: {
openURL(text_fragment_url() ?? url)
}, label: {
HStack(spacing: 10) {
if let url = get_url_icon() {
KFAnimatedImage(url)
.imageContext(.pfp, disable_animation: true)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.placeholder { _ in
Image("link")
.resizable()
.padding(5)
.foregroundColor(DamusColors.neutral6)
.background(DamusColors.adaptableWhite)
}
.frame(width: 35, height: 35)
.clipShape(RoundedRectangle(cornerRadius: 10))
.scaledToFit()
} else {
Image("link")
.resizable()
.padding(5)
.foregroundColor(DamusColors.neutral6)
.background(DamusColors.adaptableWhite)
.frame(width: 35, height: 35)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
Text(url.absoluteString)
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
.foregroundColor(DamusColors.adaptableBlack)
.truncationMode(.tail)
.lineLimit(1)
}
.padding([.leading, .vertical], 7)
.frame(maxWidth: .infinity, alignment: .leading)
.background(DamusColors.neutral3)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral3, lineWidth: 2)
)
})
}
}
struct HighlightLink_Previews: PreviewProvider {
static var previews: some View {
let url = URL(string: "https://damus.io")!
VStack {
HighlightLink(state: test_damus_state, url: url, content: "")
}
}
}
@@ -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()
}
}
}
@@ -0,0 +1,192 @@
//
// HighlightView.swift
// damus
//
// Created by eric on 4/22/24.
//
import SwiftUI
import Kingfisher
struct HighlightTruncatedText: View {
let attributedString: AttributedString
let maxChars: Int
init(attributedString: AttributedString, maxChars: Int = 360) {
self.attributedString = attributedString
self.maxChars = maxChars
}
var body: some View {
VStack(alignment: .leading) {
let truncatedAttributedString: AttributedString? = attributedString.truncateOrNil(maxLength: maxChars)
if let truncatedAttributedString {
Text(truncatedAttributedString)
.fixedSize(horizontal: false, vertical: true)
} else {
Text(attributedString)
.fixedSize(horizontal: false, vertical: true)
}
if truncatedAttributedString != nil {
Spacer()
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
.allowsHitTesting(false)
}
}
}
}
struct HighlightBodyView: View {
let state: DamusState
let event: HighlightEvent
let options: EventViewOptions
init(state: DamusState, ev: HighlightEvent, options: EventViewOptions) {
self.state = state
self.event = ev
self.options = options
}
init(state: DamusState, ev: NostrEvent, options: EventViewOptions) {
self.state = state
self.event = HighlightEvent.parse(from: ev)
self.options = options
}
var body: some View {
Group {
if options.contains(.wide) {
Main.padding(.horizontal)
} else {
Main
}
}
}
var truncate: Bool {
return options.contains(.truncate_content)
}
var truncate_very_short: Bool {
return options.contains(.truncate_content_very_short)
}
func truncatedText(attributedString: AttributedString) -> some View {
Group {
if truncate_very_short {
HighlightTruncatedText(attributedString: attributedString, maxChars: 140)
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
}
else if truncate {
HighlightTruncatedText(attributedString: attributedString)
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
} else {
Text(attributedString)
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
}
}
}
var Main: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
var attributedString: AttributedString {
var attributedString: AttributedString = ""
if let context = event.context {
if context.count < event.event.content.count {
attributedString = AttributedString(event.event.content)
} else {
attributedString = AttributedString(context)
}
} else {
attributedString = AttributedString(event.event.content)
}
if let range = attributedString.range(of: event.event.content) {
attributedString[range].backgroundColor = DamusColors.highlight
}
return attributedString
}
truncatedText(attributedString: attributedString)
.lineSpacing(5)
.padding(10)
}
.overlay(
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
alignment: .leading
)
.padding(.bottom, 10)
if let url = event.url_ref {
HighlightLink(state: state, url: url, content: event.event.content)
} else {
if let evRef = event.event_ref {
if let eventHex = hex_decode_id(evRef) {
HighlightEventRef(damus_state: state, event_ref: NoteId(eventHex))
.padding(.top, 5)
}
}
}
}
}
}
struct HighlightView: View {
let state: DamusState
let event: HighlightEvent
let options: EventViewOptions
init(state: DamusState, event: NostrEvent, options: EventViewOptions) {
self.state = state
self.event = HighlightEvent.parse(from: event)
self.options = options.union(.no_mentions)
}
var body: some View {
VStack(alignment: .leading) {
EventShell(state: state, event: event.event, options: options) {
HighlightBodyView(state: state, ev: event, options: options)
}
}
}
}
struct HighlightView_Previews: PreviewProvider {
static var previews: some View {
let content = "Nostr, a decentralized and open social network protocol. Without ads, toxic algorithms, or censorship"
let context = "Damus is built on Nostr, a decentralized and open social network protocol. Without ads, toxic algorithms, or censorship, Damus gives you access to the social network that a truly free and healthy society needs — and deserves."
let test_highlight_event = HighlightEvent.parse(from: NostrEvent(
content: content,
keypair: test_keypair,
kind: NostrKind.highlight.rawValue,
tags: [
["context", context],
["r", "https://damus.io"],
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
])!
)
let test_highlight_event2 = HighlightEvent.parse(from: NostrEvent(
content: content,
keypair: test_keypair,
kind: NostrKind.highlight.rawValue,
tags: [
["context", context],
["e", "36017b098859d62e1dbd802290d59c9de9f18bb0ca00ba4b875c2930dd5891ae"],
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
])!
)
VStack {
HighlightView(state: test_damus_state, event: test_highlight_event.event, options: [])
HighlightView(state: test_damus_state, event: test_highlight_event2.event, options: [.wide])
}
}
}
@@ -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)
}
+10 -1
View File
@@ -41,7 +41,16 @@ struct SelectedEventView: View {
.lineLimit(1)
if let reply_ref = event.thread_reply()?.reply {
ReplyDescription(event: event, replying_to: damus.events.lookup(reply_ref.note_id), ndb: damus.ndb)
let replying_to = damus.events.lookup(reply_ref.note_id)
if event.known_kind == .highlight {
HighlightDescription(event: event, highlighted_event: replying_to, ndb: damus.ndb)
.padding(.horizontal)
} else {
ReplyDescription(event: event, replying_to: replying_to, ndb: damus.ndb)
.padding(.horizontal)
}
} else if event.known_kind == .highlight {
HighlightDescription(event: event, highlighted_event: nil, ndb: damus.ndb)
.padding(.horizontal)
}
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -26,7 +26,7 @@ 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: nil, attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline)
if truncated_about != nil {
if show_full_about {
+1 -1
View File
@@ -14,7 +14,7 @@ extension NdbNote {
}
func get_cached_inner_event(cache: EventCache) -> NdbNote? {
guard self.known_kind == .boost else {
guard self.known_kind == .boost || self.known_kind == .highlight else {
return nil
}
+1 -1
View File
@@ -277,7 +277,7 @@ class NdbNote: Encodable, Equatable, Hashable {
// Extension to make NdbNote compatible with NostrEvent's original API
extension NdbNote {
var is_textlike: Bool {
return kind == 1 || kind == 42 || kind == 30023
return kind == 1 || kind == 42 || kind == 30023 || kind == 9802
}
var is_quote_repost: NoteId? {