[PATCH damus v2] Highlights

2 views
Skip to first unread message

ericholguin

unread,
May 21, 2024, 10:39:10 PMMay 21
to pat...@damus.io, ericholguin
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

Signed-off-by: ericholguin <erich...@apache.org>
---
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 | 2 +-
damus/Models/SearchModel.swift | 2 +-
damus/Nostr/NostrKind.swift | 1 +
damus/Views/EventView.swift | 2 +
damus/Views/Events/Components/ReplyPart.swift | 4 +-
damus/Views/Events/EventBody.swift | 2 +
.../Highlight/HighlightDescription.swift | 54 +++++
.../Events/Highlight/HighlightEventRef.swift | 92 +++++++++
.../Events/Highlight/HighlightLink.swift | 101 ++++++++++
.../Events/Highlight/HighlightView.swift | 184 ++++++++++++++++++
damus/Views/Events/SelectedEventView.swift | 10 +-
nostrdb/NdbNote+.swift | 2 +-
nostrdb/NdbNote.swift | 2 +-
18 files changed, 556 insertions(+), 8 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 682ca590..439cb295 100644
--- a/damus.xcodeproj/project.pbxproj
+++ b/damus.xcodeproj/project.pbxproj
@@ -405,6 +405,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 */; };
@@ -1329,6 +1334,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>"; };
@@ -1649,6 +1659,8 @@
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */,
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
B533694D2B66D791008A805E /* MutelistManager.swift */,
+ D7C28E3A2BBB4D0000EE459F /* VideoCache.swift */,
+ 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -2374,6 +2386,7 @@
4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup;
children = (
+ 5CC852A02BDED9970039FFC5 /* Highlight */,
4CA927682A290F8F0098A105 /* Components */,
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
@@ -2669,6 +2682,17 @@
path = Images;
sourceTree = "<group>";
};
+ 5CC852A02BDED9970039FFC5 /* Highlight */ = {
+ isa = PBXGroup;
+ children = (
+ 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */,
+ 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */,
+ 5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */,
+ 5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */,
+ );
+ path = Highlight;
+ sourceTree = "<group>";
+ };
7C0F392D29B57C8F0039859C /* Extensions */ = {
isa = PBXGroup;
children = (
@@ -3134,6 +3158,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 */,
@@ -3269,6 +3294,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 */,
@@ -3311,7 +3337,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 */,
@@ -3424,6 +3452,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 1ae8e79f..a632fbed 100644
--- a/damus/Components/DamusColors.swift
+++ b/damus/Components/DamusColors.swift
@@ -23,6 +23,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 82d5bde1..3ed15cf6 100644
--- a/damus/Models/HomeModel.swift
+++ b/damus/Models/HomeModel.swift
@@ -180,7 +180,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)
@@ -581,7 +581,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..94564b44 100644
--- a/damus/Models/ProfileModel.swift
+++ b/damus/Models/ProfileModel.swift
@@ -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]
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 74cf7f0e..dab93050 100644
--- a/damus/Views/Events/Components/ReplyPart.swift
+++ b/damus/Views/Events/Components/ReplyPart.swift
@@ -23,8 +23,10 @@ struct ReplyPart: View {

var body: some View {
Group {
- if event_is_reply(event.event_refs(keypair)) {
+ if event_is_reply(event.event_refs(keypair)) && 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()
}
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..1d0c7baa
--- /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..c2041d54
--- /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..e3510826
--- /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..38c76fe0
--- /dev/null
+++ b/damus/Views/Events/Highlight/HighlightView.swift
@@ -0,0 +1,184 @@
+//
+// 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(event.context ?? 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 18e6d81a..54eee368 100644
--- a/damus/Views/Events/SelectedEventView.swift
+++ b/damus/Views/Events/SelectedEventView.swift
@@ -49,7 +49,15 @@ struct SelectedEventView: View {
.lineLimit(1)

if event_is_reply(event.event_refs(damus.keypair)) {
- ReplyDescription(event: event, replying_to: replying_to, ndb: damus.ndb)
+ 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 4ce5e726..0e170d8e 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? {
--
2.39.3 (Apple Git-145)

Daniel D'Aquino

unread,
May 24, 2024, 8:29:26 PMMay 24
to Eric Holguin, sembene_truestar via patches
Hi Eric,

Thank you for looking into these!

On May 21, 2024, at 19:47, Eric Holguin <ericholgu...@gmail.com> wrote:

Hey Daniel,
 
I’ve addressed some of your review comments:
  • Creating the highlight
    • This is not included in this patch; it will be a separate patch.

Sounds good!
  • Can't select highlighted text on the selected event view
    • Due to the HighlightView not using NoteContentView highlights aren’t selectable, I did try adding the text into the SelectableText but that causes us to lose the highlighted attributed text, I think it would be a more complex task to add this feature.

Yeah, maybe this can be delegated to a future ticket.
  • “Highlighted" visual indicator under the user display name is not visible on the selected event view
    • This has been addressed

Thanks!
  • Relative date display is missing on highlight events (e.g. “1h”, “30min”)
    • This is also the case with longform events not sure what the issue is here but I think it is a separate bug.

Makes sense, do we have a ticket open for this?
  • Very long highlights are not truncated
    • This has been addressed

Thanks for looking into these! Do we have tickets for items that we might want to fix in the future?

  • Highlight content is cutoff early on newline characters
    • I’m not sure what the issue was here

Here is one example note: note10kejgfl2n0f08djslfaaxevgv3j97nfwgvr0zlvy5l8pw6tfjaksulqv70

Here are the JSON contents of that note:

{
  "created_at":1715630330,
  "content":"Feugiat nibh sed pulvinar proin gravida hendrerit lectus a.\nVitae turpis massa sed elementum tempus egestas sed.\nHendrerit dolor magna eget est lorem ipsum. Nunc vel risus commodo viverra maecenas accumsan lacus vel. Nullam eget felis eget nunc. Sociis natoque penatibus et magnis dis parturient.",
  "tags": [
      ["p","79c316c262c3fb8894cd302b3da1ba373d74d4103ace20eee4895f563e1c4314"],
      ["alt","\"Feugiat nibh sed pulvinar proin gravida hendrerit lectus a.\nVitae turpis massa sed elementum tempus egestas sed.\nHendrerit dolor magna eget est lorem ipsum. Nunc vel risus commodo viverra maecenas accumsan lacus vel. Nullam eget felis eget nunc. Sociis natoque penatibus et magnis dis parturient.\"\n\nThis is a highlight created in https:\/\/habla.news"],
      ["client","31990:7d4e04503ab26615dd5f29ec08b52943cbe5f17bacc3012b26220caa232ab14c:1687329691033","wss:\/\/relay.nostr.band","web"],
      ["a","30023:79c316c262c3fb8894cd302b3da1ba373d74d4103ace20eee4895f563e1c4314:1715629103314","wss:\/\/offchain.pub"],
      ["context","Feugiat nibh sed pulvinar proin gravida hendrerit lectus a."]],
  "sig":"07d0fa038304ec1e7aad020376f694d0e25c6abfc0bec87454e52a9d089e68b16129dc6a73f451b8c811b486c9a1c43ce68952f3e56de5c387364c470eeb9d6b",
  "kind":9802,
  "pubkey":"79c316c262c3fb8894cd302b3da1ba373d74d4103ace20eee4895f563e1c4314",
  "id":"7db32427ea9bd2f3b650fa7bd3658864645f4d2e4306f17d84a7ce176969976d"
}

The highlighted content is a multiline text.

However, when rendering this highlight note, this is what I see:

Simulator Screenshot - iPhone 15 - 2024-05-24 at 17.23.29.png

It seems to be only displaying a portion of the highlighted content, not the whole content. I tested this with a few different highlights and it seems that it ignores all content text after a newline character (`\n`)

I could be misinterpreting things though, I am not 100% familiar with this part of the protocol.


Regards,
Eric Holguin
 
 

"],

To unsubscribe from this group and stop receiving emails from it, send an email to patches+u...@damus.io.


Reply all
Reply to author
Forward
0 new messages