[PATCH damus v1] Highlights

10 views
Skip to first unread message

ericholguin

unread,
Apr 29, 2024, 10:06:48 PMApr 29
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)

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 | 130 ++++++++++++++++++
damus/Views/Events/SelectedEventView.swift | 9 +-
nostrdb/NdbNote+.swift | 2 +-
nostrdb/NdbNote.swift | 2 +-
18 files changed, 500 insertions(+), 9 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..677c2768
--- /dev/null
+++ b/damus/Views/Events/Highlight/HighlightView.swift
@@ -0,0 +1,130 @@
+//
+// HighlightView.swift
+// damus
+//
+// Created by eric on 4/22/24.
+//
+
+import SwiftUI
+import Kingfisher
+
+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 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
+ }
+
+ Text(attributedString)
+ .font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
+ .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..c90699db 100644
--- a/damus/Views/Events/SelectedEventView.swift
+++ b/damus/Views/Events/SelectedEventView.swift
@@ -49,8 +49,13 @@ struct SelectedEventView: View {
.lineLimit(1)

if event_is_reply(event.event_refs(damus.keypair)) {
- ReplyDescription(event: event, replying_to: replying_to, ndb: damus.ndb)
- .padding(.horizontal)
+ 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)
+ }
}

ProxyView(event: event)
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)

William Casarin

unread,
May 3, 2024, 11:42:16 AMMay 3
to ericholguin, pat...@damus.io, Daniel Daquino
On Mon, Apr 29, 2024 at 08:06:40PM -0600, ericholguin wrote:
>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)
>
>Signed-off-by: ericholguin <erich...@apache.org>
>---

Very slick, will test this soon.

Daniel D'Aquino

unread,
May 3, 2024, 10:49:22 PMMay 3
to William Casarin, ericholguin, sembene_truestar via patches
On May 3, 2024, at 08:42, William Casarin <jb...@jb55.com> wrote:

On Mon, Apr 29, 2024 at 08:06:40PM -0600, ericholguin wrote:
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)

Signed-off-by: ericholguin <erich...@apache.org>
---

Very slick, will test this soon.

I was also going to help test this, but I can’t get the patch to apply no matter how I try. I don’t have the original email, so I have been trying to get the separate pieces together from this, but I always get some kind of error.

If possible, can either of you send me the original patch? I apologize for the inconvenience.


(I don’t have the original email because I had my Google Groups setting to send me a daily digest instead. I changed that back to avoid this issue in the future)

Thanks!

Daniel D'Aquino

unread,
May 13, 2024, 4:56:02 PMMay 13
to ericholguin, sembene_truestar via patches, William Casarin

On Apr 29, 2024, at 19:06, ericholguin <erich...@apache.org> wrote:

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)

Signed-off-by: ericholguin <erich...@apache.org>

I have completed some functional testing. Overall it works quite well and I really like the UX around highlights are displayed

There are a few technical issues/remaining items I have encountered. Some of them are things we have already discussed, but I will include them in my report for the sake of completeness.


Testing

Preconditions: A mix of these
  1. Device: iPhone 13 mini
    iOS: 17.4.1
    Damus: 5fb0f5eb8f3db8947a434be41c801a5464a2ec86 (This patch applied on top of 6d5a152c17)
  1. Device: iPhone 15 simulator
    iOS: 17.4
    Damus: 5fb0f5eb8f3db8947a434be41c801a5464a2ec86 (This patch applied on top of 6d5a152c17)
Other setup notes:
  • Highlights made using highlighter.com and habla news
  • Long form posts made using habla news
Coverage:
  • Quoting highlight — works well
  • Reposting the highlight — works well
  • Reposting the quoted highlight — works well
  • Bookmarking the highlight and viewing that bookmark — works well
  • Creating the highlight — I could not do it on mine. Perhaps that feature is not yet included in this patch?
  • Highlight display — Encountered a few issues:
    • Can't select highlighted text on the selected event view
    • “Highlighted" visual indicator under the user display name is not visible on the selected event view
    • Relative date display is missing on highlight events (e.g. “1h”, “30min”)
  • Long highlights — Encountered a few issues:
    • Very long highlights are not truncated
      • Example event: note1jggrutkg6q304zsc7y3ed9fccweh9jypy49mws3zx5kud39ysc7sc899ph
    • Highlight content is cutoff early on newline characters
      • Example event: note10kejgfl2n0f08djslfaaxevgv3j97nfwgvr0zlvy5l8pw6tfjaksulqv70

Overall I think this is very nice and we seem to be very close!


Please let me know if you need more help with testing this, or if you have questions about any of the above.

Thank you,
Daniel

William Casarin

unread,
May 21, 2024, 3:00:13 PMMay 21
to Daniel D'Aquino, ericholguin, pat...@damus.io, d...@damus.io
On Sat, May 04, 2024 at 02:49:12AM GMT, Daniel D'Aquino wrote:
>> On May 3, 2024, at 08:42, William Casarin <jb...@jb55.com> wrote:
>> On Mon, Apr 29, 2024 at 08:06:40PM -0600, ericholguin wrote:
>>
>>> 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.
>>>
>>> [..]
>>
>> Very slick, will test this soon.
>
>I was also going to help test this, but I can’t get the patch to apply
>no matter how I try. I don’t have the original email, so I have been
>trying to get the separate pieces together from this, but I always get
>some kind of error.
>
>If possible, can either of you send me the original patch? I apologize
>for the inconvenience.
>
>(I don’t have the original email because I had my Google Groups setting
>to send me a daily digest instead. I changed that back to avoid this
>issue in the future)
>
>Thanks!

github PRs should sove this problem, we need it for CI anyway so we
might as well just use it. If github ever bans us we can figure out
another CI solution and fallback to email.

There is also a really cool trick you can do:

git config --add remote.origin.fetch '+refs/pull/*/head:refs/remotes/pr/*'

This will mark all github PRs in your tree when `git log --graph`:

https://cdn.jb55.com/s/8662c34eb2ee7299.png
Reply all
Reply to author
Forward
0 new messages