From 6376c61badff4ecc52ccc98d016f5aecd4339705 Mon Sep 17 00:00:00 2001 From: ericholguin Date: Mon, 27 May 2024 13:05:59 -0600 Subject: [PATCH 1/4] Highlights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds highlights (NIP-84) to Damus. Kind 9802 are handled by all the necessary models. We show highlighted events, longform events, and url references. Url references also leverage text fragments to take the user to the highlighted text. Testing —— iPhone 15 Pro Max (17.0) Dark Mode: https://v.nostr.build/oM6DW.mp4 iPhone 15 Pro Max (17.0) Light Mode: https://v.nostr.build/BRrmP.mp4 iPhone SE (3rd generation) (16.4) Light Mode: https://v.nostr.build/6GzKa.mp4 —— Closes: https://github.com/damus-io/damus/issues/2172 Closes: https://github.com/damus-io/damus/issues/1772 Closes: https://github.com/damus-io/damus/issues/1773 Closes: https://github.com/damus-io/damus/issues/2173 Closes: https://github.com/damus-io/damus/issues/2175 Changelog-Added: Highlights (NIP-84) PATCH CHANGELOG: V1 -> V2: addressed review comments highlights are now truncated and highlight label shown in Thread view V2 -> V3: handle case where highlight context is smaller than the highlight content Signed-off-by: ericholguin --- damus.xcodeproj/project.pbxproj | 29 +++ .../DamusHighlight.colorset/Contents.json | 38 ++++ damus/Components/DamusColors.swift | 1 + damus/Models/HighlightEvent.swift | 34 ++++ damus/Models/HomeModel.swift | 4 +- damus/Models/ProfileModel.swift | 6 +- damus/Models/SearchModel.swift | 2 +- damus/Nostr/NostrKind.swift | 1 + damus/Views/EventView.swift | 2 + damus/Views/Events/Components/ReplyPart.swift | 10 +- damus/Views/Events/EventBody.swift | 2 + .../Highlight/HighlightDescription.swift | 54 +++++ .../Events/Highlight/HighlightEventRef.swift | 92 +++++++++ .../Events/Highlight/HighlightLink.swift | 101 +++++++++ .../Events/Highlight/HighlightView.swift | 192 ++++++++++++++++++ damus/Views/Events/SelectedEventView.swift | 11 +- nostrdb/NdbNote+.swift | 2 +- nostrdb/NdbNote.swift | 2 +- 18 files changed, 573 insertions(+), 10 deletions(-) create mode 100644 damus/Assets.xcassets/Colors/DamusHighlight.colorset/Contents.json create mode 100644 damus/Models/HighlightEvent.swift create mode 100644 damus/Views/Events/Highlight/HighlightDescription.swift create mode 100644 damus/Views/Events/Highlight/HighlightEventRef.swift create mode 100644 damus/Views/Events/Highlight/HighlightLink.swift create mode 100644 damus/Views/Events/Highlight/HighlightView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 290d3a7f..fae18171 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -406,6 +406,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 */; }; @@ -1341,6 +1346,11 @@ 5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = ""; }; 5C7389B62B9E692E00781E0A /* MutinyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyButton.swift; sourceTree = ""; }; 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyGradient.swift; sourceTree = ""; }; + 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEvent.swift; sourceTree = ""; }; + 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightView.swift; sourceTree = ""; }; + 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDescription.swift; sourceTree = ""; }; + 5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightLink.swift; sourceTree = ""; }; + 5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEventRef.swift; sourceTree = ""; }; 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeutralButtonStyle.swift; sourceTree = ""; }; 5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicView.swift; sourceTree = ""; }; 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLightGradient.swift; sourceTree = ""; }; @@ -1668,6 +1678,8 @@ B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */, B533694D2B66D791008A805E /* MutelistManager.swift */, D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */, + D7C28E3A2BBB4D0000EE459F /* VideoCache.swift */, + 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */, ); path = Models; sourceTree = ""; @@ -2402,6 +2414,7 @@ 4CC7AAEE297F11B300430951 /* Events */ = { isa = PBXGroup; children = ( + 5CC852A02BDED9970039FFC5 /* Highlight */, 4CA927682A290F8F0098A105 /* Components */, 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */, 4CC7AAF5297F1A6A00430951 /* EventBody.swift */, @@ -2699,6 +2712,17 @@ path = Images; sourceTree = ""; }; + 5CC852A02BDED9970039FFC5 /* Highlight */ = { + isa = PBXGroup; + children = ( + 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */, + 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */, + 5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */, + 5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */, + ); + path = Highlight; + sourceTree = ""; + }; 7C0F392D29B57C8F0039859C /* Extensions */ = { isa = PBXGroup; children = ( @@ -3183,6 +3207,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 +3347,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 +3391,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 +3505,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 */, diff --git a/damus/Assets.xcassets/Colors/DamusHighlight.colorset/Contents.json b/damus/Assets.xcassets/Colors/DamusHighlight.colorset/Contents.json new file mode 100644 index 00000000..3acd3c0d --- /dev/null +++ b/damus/Assets.xcassets/Colors/DamusHighlight.colorset/Contents.json @@ -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 + } +} diff --git a/damus/Components/DamusColors.swift b/damus/Components/DamusColors.swift index fed3d2ba..860ff510 100644 --- a/damus/Components/DamusColors.swift +++ b/damus/Components/DamusColors.swift @@ -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") diff --git a/damus/Models/HighlightEvent.swift b/damus/Models/HighlightEvent.swift new file mode 100644 index 00000000..40bd0100 --- /dev/null +++ b/damus/Models/HighlightEvent.swift @@ -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 + } +} diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index 85798c9d..aa375707 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -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) diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift index 4781168d..90376383 100644 --- a/damus/Models/ProfileModel.swift +++ b/damus/Models/ProfileModel.swift @@ -60,11 +60,11 @@ class ProfileModel: ObservableObject, Equatable { damus.pool.unsubscribe(sub_id: sub_id) damus.pool.unsubscribe(sub_id: prof_subid) } - + 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] text_filter.authors = [pubkey] diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift index 0f02dfb5..ab971bff 100644 --- a/damus/Models/SearchModel.swift +++ b/damus/Models/SearchModel.swift @@ -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! diff --git a/damus/Nostr/NostrKind.swift b/damus/Nostr/NostrKind.swift index 18578d8d..4138fb36 100644 --- a/damus/Nostr/NostrKind.swift +++ b/damus/Nostr/NostrKind.swift @@ -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 diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift index e3996b55..72c9c543 100644 --- a/damus/Views/EventView.swift +++ b/damus/Views/EventView.swift @@ -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) diff --git a/damus/Views/Events/Components/ReplyPart.swift b/damus/Views/Events/Components/ReplyPart.swift index dbc6f590..7fa8bffd 100644 --- a/damus/Views/Events/Components/ReplyPart.swift +++ b/damus/Views/Events/Components/ReplyPart.swift @@ -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() } diff --git a/damus/Views/Events/EventBody.swift b/damus/Views/Events/EventBody.swift index ffba153d..31c978b5 100644 --- a/damus/Views/Events/EventBody.swift +++ b/damus/Views/Events/EventBody.swift @@ -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 } diff --git a/damus/Views/Events/Highlight/HighlightDescription.swift b/damus/Views/Events/Highlight/HighlightDescription.swift new file mode 100644 index 00000000..756ea988 --- /dev/null +++ b/damus/Views/Events/Highlight/HighlightDescription.swift @@ -0,0 +1,54 @@ +// +// 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 desc.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 = NSOrderedSet(array: names).array as! [String] + + return String(format: NSLocalizedString("Highlighted %@", bundle: bundle, comment: "Label to indicate that the user is highlighting 1 user."), locale: locale, uniqueNames[0]) +} diff --git a/damus/Views/Events/Highlight/HighlightEventRef.swift b/damus/Views/Events/Highlight/HighlightEventRef.swift new file mode 100644 index 00000000..aa0c9203 --- /dev/null +++ b/damus/Views/Events/Highlight/HighlightEventRef.swift @@ -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() + } + } + } + } +} diff --git a/damus/Views/Events/Highlight/HighlightLink.swift b/damus/Views/Events/Highlight/HighlightLink.swift new file mode 100644 index 00000000..3e75e2c4 --- /dev/null +++ b/damus/Views/Events/Highlight/HighlightLink.swift @@ -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: "") + } + } +} diff --git a/damus/Views/Events/Highlight/HighlightView.swift b/damus/Views/Events/Highlight/HighlightView.swift new file mode 100644 index 00000000..441d7041 --- /dev/null +++ b/damus/Views/Events/Highlight/HighlightView.swift @@ -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]) + } + } +} diff --git a/damus/Views/Events/SelectedEventView.swift b/damus/Views/Events/SelectedEventView.swift index e7fca471..ceb34828 100644 --- a/damus/Views/Events/SelectedEventView.swift +++ b/damus/Views/Events/SelectedEventView.swift @@ -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) } diff --git a/nostrdb/NdbNote+.swift b/nostrdb/NdbNote+.swift index 79b2d4c7..79301f69 100644 --- a/nostrdb/NdbNote+.swift +++ b/nostrdb/NdbNote+.swift @@ -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 } diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift index f11a071d..ab2c15f1 100644 --- a/nostrdb/NdbNote.swift +++ b/nostrdb/NdbNote.swift @@ -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? { From 0dce7aea4561574b295cb25ef57e84d2bb531da6 Mon Sep 17 00:00:00 2001 From: ericholguin Date: Mon, 27 May 2024 13:06:00 -0600 Subject: [PATCH 2/4] 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 { From e7ed9dfe86e71328d5408cc15eda81ff5798dbfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Fri, 21 Jun 2024 14:11:45 -0700 Subject: [PATCH 3/4] Small tweaks for better code safety --- damus/Views/Events/Highlight/HighlightDescription.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/damus/Views/Events/Highlight/HighlightDescription.swift b/damus/Views/Events/Highlight/HighlightDescription.swift index 756ea988..5a6d1e19 100644 --- a/damus/Views/Events/Highlight/HighlightDescription.swift +++ b/damus/Views/Events/Highlight/HighlightDescription.swift @@ -34,7 +34,7 @@ func highlight_desc(ndb: Ndb, event: NostrEvent, highlighted_event: NostrEvent?, let bundle = bundleForLocale(locale: locale) - if desc.pubkeys.count == 0 { + if pubkeys.count == 0 { return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.") } @@ -48,7 +48,6 @@ func highlight_desc(ndb: Ndb, event: NostrEvent, highlighted_event: NostrEvent?, return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50) } - let uniqueNames = NSOrderedSet(array: names).array as! [String] - - return String(format: NSLocalizedString("Highlighted %@", bundle: bundle, comment: "Label to indicate that the user is highlighting 1 user."), locale: locale, uniqueNames[0]) + 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 ?? "") } From 4ccfe81558427a8f5255b48ab55b614c11f6fe29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Fri, 21 Jun 2024 14:18:36 -0700 Subject: [PATCH 4/4] Allow highlighting to be disabled on SelectableText Changed the interface of SelectableText to allow highlighting to be disabled in places where it is not applicable (For example, on the AboutView). This prevents the need for adding dummy events in places where highlighting is not applicable, preventing the user from making bad highlights. Testing ------- PASS Device: iPhone 13 mini iOS: 17.5 Damus: This version Steps: 1. Go to a user profile and select some text in their bio. The "highlight" option should not be present. 2. Go to a note and select some text. The "highlight" option should be available --- damus/Components/SelectableText.swift | 20 ++++++++++++++------ damus/Views/Profile/AboutView.swift | 6 +----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/damus/Components/SelectableText.swift b/damus/Components/SelectableText.swift index 05500eb9..db62aa96 100644 --- a/damus/Components/SelectableText.swift +++ b/damus/Components/SelectableText.swift @@ -10,7 +10,7 @@ import SwiftUI struct SelectableText: View { let damus_state: DamusState - let event: NostrEvent + let event: NostrEvent? let attributedString: AttributedString let textAlignment: NSTextAlignment @State private var showHighlightPost = false @@ -20,7 +20,7 @@ struct SelectableText: View { let size: EventViewKind - init(damus_state: DamusState, event: NostrEvent, 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 @@ -36,6 +36,7 @@ struct SelectableText: View { font: eventviewsize_to_uifont(size), fixedWidth: selectedTextWidth, textAlignment: self.textAlignment, + enableHighlighting: self.enableHighlighting(), showHighlightPost: $showHighlightPost, selectedText: $selectedText, height: $selectedTextHeight @@ -53,12 +54,18 @@ struct SelectableText: View { } } .sheet(isPresented: $showHighlightPost) { - HighlightPostView(damus_state: damus_state, event: event, selectedText: $selectedText) - .presentationDragIndicator(.visible) - .presentationDetents([.height(selectedTextHeight + 150), .medium, .large]) + 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 { @@ -97,6 +104,7 @@ fileprivate class TextView: UITextView { let font: UIFont let fixedWidth: CGFloat let textAlignment: NSTextAlignment + let enableHighlighting: Bool @Binding var showHighlightPost: Bool @Binding var selectedText: String @Binding var height: CGFloat @@ -115,7 +123,7 @@ fileprivate class TextView: UITextView { let menuController = UIMenuController.shared let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:))) - menuController.menuItems = [highlightItem] + menuController.menuItems = self.enableHighlighting ? [highlightItem] : [] return view } diff --git a/damus/Views/Profile/AboutView.swift b/damus/Views/Profile/AboutView.swift index 892d23f0..d479c5bf 100644 --- a/damus/Views/Profile/AboutView.swift +++ b/damus/Views/Profile/AboutView.swift @@ -26,11 +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(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) + 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 {