From b43dcd2bc710a8ebf2f7688981190317d9fc72c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Thu, 22 Aug 2024 14:34:10 -0700 Subject: [PATCH] Fix highlight tag ambiguity with specifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes the ambiguity in tags used in highlights with comments, by adding specifiers to help clients understand: - If a URL reference is the source of the highlight or just a URL mentioned in the comment - If a pubkey reference is the author of the highlighted content, or just a generic mention in the comment This tries to be backwards compatible with previous versions of NIP-84. Testing -------- PASS Device: iPhone 15 simulator iOS: 17.5 Damus: This commit Steps: 1. Create a new highlight from a webpage using the extension. Tag a user and attach an image 2. Check the newly-created highlight: 1. Highlight description line should just say "Highlighted", not "Highlighted " 2. Highlight source link preview should present the URL of the highlighted page, NOT the image URL 3. Inspect the JSON for the newly-created highlight: 1. "r" tags should include specifiers in the 3rd slot, such as "source" or "mention" 2. "p" tags should include specifiers in the 3rd slot, such as "mention" 4. Go to an older, generic highlight (without comment) to another nostr event and check the view. 1. Highlight description line should say "Highlighted " 2. Clicking on the highlight should lead to the highlighted event itself. Signed-off-by: Daniel D’Aquino --- damus/Models/HighlightEvent.swift | 159 +++++++++++++++++- damus/Models/Mentions.swift | 34 ---- damus/Models/Post.swift | 46 ++++- damus/Views/Events/Components/ReplyPart.swift | 3 +- .../Highlight/HighlightDescription.swift | 30 +--- damus/Views/PostView.swift | 12 +- 6 files changed, 212 insertions(+), 72 deletions(-) diff --git a/damus/Models/HighlightEvent.swift b/damus/Models/HighlightEvent.swift index 024c7273..a269c665 100644 --- a/damus/Models/HighlightEvent.swift +++ b/damus/Models/HighlightEvent.swift @@ -13,24 +13,176 @@ struct HighlightEvent { var event_ref: String? = nil var url_ref: URL? = nil var context: String? = nil + + // MARK: - Initializers and parsers static func parse(from ev: NostrEvent) -> HighlightEvent { var highlight = HighlightEvent(event: ev) + + var best_url_source: (url: URL, tagged_as_source: Bool)? = nil 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 "r": + if tag.count >= 3, + tag[2].string() == HighlightSource.TAG_SOURCE_ELEMENT, + let url = URL(string: tag[1].string()) { + // URL marked as source. Very good candidate + best_url_source = (url: url, tagged_as_source: true) + } + else if tag.count >= 3 && tag[2].string() != HighlightSource.TAG_SOURCE_ELEMENT { + // URL marked as something else (not source). Not the source we are after + } + else if let url = URL(string: tag[1].string()), tag.count == 2 { + // Unmarked URL. This might be what we are after (For NIP-84 backwards compatibility) + if (best_url_source?.tagged_as_source ?? false) == false { + // No URL candidates marked as the source. Mark this as the best option we have + best_url_source = (url: url, tagged_as_source: false) + } + } case "context": highlight.context = tag[1].string() default: break } } + + if let best_url_source { + highlight.url_ref = best_url_source.url + } return highlight } + + // MARK: - Getting information about source + + func source_description_info(highlighted_event: NostrEvent?) -> ReplyDesc { + var others_count = 0 + var highlighted_authors: [Pubkey] = [] + var i = event.tags.count + + if let highlighted_event { + highlighted_authors.append(highlighted_event.pubkey) + } + + for tag in event.tags { + if let pubkey_with_role = PubkeyWithRole.from_tag(tag: tag) { + others_count += 1 + if highlighted_authors.count < 2 { + if let highlighted_event, pubkey_with_role.pubkey == highlighted_event.pubkey { + continue + } else { + switch pubkey_with_role.role { + case .author: + highlighted_authors.append(pubkey_with_role.pubkey) + default: + break + } + + } + } + } + i -= 1 + } + + return ReplyDesc(pubkeys: highlighted_authors, others: others_count) + } + + func source_description_text(ndb: Ndb, highlighted_event: NostrEvent?, locale: Locale = Locale.current) -> String { + let description_info = self.source_description_info(highlighted_event: highlighted_event) + let pubkeys = description_info.pubkeys + + let bundle = bundleForLocale(locale: locale) + + if pubkeys.count == 0 { + return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.") + } + + guard let profile_txn = NdbTxn(ndb: ndb) else { + return "" + } + + let names: [String] = pubkeys.map { pk in + let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn) + + return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50) + } + + let uniqueNames: [String] = Array(Set(names)) + return String(format: NSLocalizedString("Highlighted %@", bundle: bundle, comment: "Label to indicate that the user is highlighting 1 user."), locale: locale, uniqueNames.first ?? "") + } +} + +// MARK: - Helper structures + +extension HighlightEvent { + struct PubkeyWithRole: TagKey, TagConvertible { + let pubkey: Pubkey + let role: Role + + var tag: [String] { + if let role_text = self.role.rawValue { + return [keychar.description, self.pubkey.hex(), role_text] + } + else { + return [keychar.description, self.pubkey.hex()] + } + } + + var keychar: AsciiCharacter { "p" } + + static func from_tag(tag: TagSequence) -> HighlightEvent.PubkeyWithRole? { + var i = tag.makeIterator() + + guard tag.count >= 2, + let t0 = i.next(), + let key = t0.single_char, + key == "p", + let t1 = i.next(), + let pubkey = t1.id().map(Pubkey.init) + else { return nil } + + let t3: String? = i.next()?.string() + let role = Role(rawValue: t3) + return PubkeyWithRole(pubkey: pubkey, role: role) + } + + enum Role: RawRepresentable { + case author + case editor + case mention + case other(String) + case no_role + + typealias RawValue = String? + var rawValue: String? { + switch self { + case .author: "author" + case .editor: "editor" + case .mention: "mention" + case .other(let role): role + case .no_role: nil + } + } + + init(rawValue: String?) { + switch rawValue { + case "author": self = .author + case "editor": self = .editor + case "mention": self = .mention + default: + if let rawValue { + self = .other(rawValue) + } + else { + self = .no_role + } + } + } + } + } } struct HighlightContentDraft: Hashable { @@ -39,15 +191,16 @@ struct HighlightContentDraft: Hashable { } enum HighlightSource: Hashable { + static let TAG_SOURCE_ELEMENT = "source" case event(NostrEvent) case external_url(URL) func tags() -> [[String]] { switch self { case .event(let event): - return [ ["e", "\(event.id)"] ] + return [ ["e", "\(event.id)", HighlightSource.TAG_SOURCE_ELEMENT] ] case .external_url(let url): - return [ ["r", "\(url)"] ] + return [ ["r", "\(url)", HighlightSource.TAG_SOURCE_ELEMENT] ] } } diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift index 2f0785f1..7cfcf722 100644 --- a/damus/Models/Mentions.swift +++ b/damus/Models/Mentions.swift @@ -256,37 +256,3 @@ func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? { return nil } - -struct PostTags { - let blocks: [Block] - let tags: [[String]] -} - -/// Convert -func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags { - var new_tags = tags - - for post_block in post_blocks { - switch post_block { - case .mention(let mention): - switch(mention.ref) { - case .note, .nevent: - continue - default: - break - } - - new_tags.append(mention.ref.tag) - case .hashtag(let hashtag): - new_tags.append(["t", hashtag.lowercased()]) - case .text: break - case .invoice: break - case .relay: break - case .url(let url): - new_tags.append(["r", url.absoluteString]) - break - } - } - - return PostTags(blocks: post_blocks, tags: new_tags) -} diff --git a/damus/Models/Post.swift b/damus/Models/Post.swift index 6a222e93..acb6ac6c 100644 --- a/damus/Models/Post.swift +++ b/damus/Models/Post.swift @@ -20,7 +20,7 @@ struct NostrPost { func to_event(keypair: FullKeypair) -> NostrEvent? { let post_blocks = self.parse_blocks() - let post_tags = make_post_tags(post_blocks: post_blocks, tags: self.tags) + let post_tags = self.make_post_tags(post_blocks: post_blocks, tags: self.tags) let content = post_tags.blocks .map(\.asString) .joined(separator: "") @@ -49,6 +49,50 @@ struct NostrPost { return self.content } } + + /// Parse the post's contents to find more tags to apply to the final nostr event + private func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags { + var new_tags = tags + + for post_block in post_blocks { + switch post_block { + case .mention(let mention): + switch(mention.ref) { + case .note, .nevent: + continue + default: + break + } + + if self.kind == .highlight, case .pubkey(_) = mention.ref { + var new_tag = mention.ref.tag + new_tag.append("mention") + new_tags.append(new_tag) + } + else { + new_tags.append(mention.ref.tag) + } + case .hashtag(let hashtag): + new_tags.append(["t", hashtag.lowercased()]) + case .text: break + case .invoice: break + case .relay: break + case .url(let url): + new_tags.append(self.kind == .highlight ? ["r", url.absoluteString, "mention"] : ["r", url.absoluteString]) + break + } + } + + return PostTags(blocks: post_blocks, tags: new_tags) + } +} + +// MARK: - Helper structures and functions + +/// A struct used for temporarily holding tag information that was parsed from a post contents to aid in building a nostr event +fileprivate struct PostTags { + let blocks: [Block] + let tags: [[String]] } func parse_post_blocks(content: String) -> [Block] { diff --git a/damus/Views/Events/Components/ReplyPart.swift b/damus/Views/Events/Components/ReplyPart.swift index 85858299..e6e00807 100644 --- a/damus/Views/Events/Components/ReplyPart.swift +++ b/damus/Views/Events/Components/ReplyPart.swift @@ -17,7 +17,8 @@ struct ReplyPart: View { Group { if event.known_kind == .highlight { let highlighted_note = event.highlighted_note_id().flatMap { events.lookup($0) } - HighlightDescription(event: event, highlighted_event: highlighted_note, ndb: ndb) + let highlight_note = HighlightEvent.parse(from: event) + HighlightDescription(highlight_event: highlight_note, highlighted_event: highlighted_note, ndb: ndb) } else if let reply_ref = event.thread_reply()?.reply { let replying_to = events.lookup(reply_ref.note_id) ReplyDescription(event: event, replying_to: replying_to, ndb: ndb) diff --git a/damus/Views/Events/Highlight/HighlightDescription.swift b/damus/Views/Events/Highlight/HighlightDescription.swift index 5a6d1e19..65d48a8e 100644 --- a/damus/Views/Events/Highlight/HighlightDescription.swift +++ b/damus/Views/Events/Highlight/HighlightDescription.swift @@ -9,12 +9,12 @@ import SwiftUI // Modified from Reply Description struct HighlightDescription: View { - let event: NostrEvent + let highlight_event: HighlightEvent 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))")) + (Text(Image(systemName: "highlighter")) + Text(verbatim: " \(highlight_event.source_description_text(ndb: ndb, highlighted_event: highlighted_event))")) .font(.footnote) .foregroundColor(.gray) .frame(maxWidth: .infinity, alignment: .leading) @@ -24,30 +24,6 @@ struct HighlightDescription: View { struct HighlightDescription_Previews: PreviewProvider { static var previews: some View { - HighlightDescription(event: test_note, highlighted_event: test_note, ndb: test_damus_state.ndb) + HighlightDescription(highlight_event: HighlightEvent.parse(from: test_note), highlighted_event: nil, ndb: test_damus_state.ndb) } } - -func highlight_desc(ndb: Ndb, event: NostrEvent, highlighted_event: NostrEvent?, locale: Locale = Locale.current) -> String { - let desc = make_reply_description(event, replying_to: highlighted_event) - let pubkeys = desc.pubkeys - - let bundle = bundleForLocale(locale: locale) - - if pubkeys.count == 0 { - return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.") - } - - guard let profile_txn = NdbTxn(ndb: ndb) else { - return "" - } - - let names: [String] = pubkeys.map { pk in - let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn) - - return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50) - } - - let uniqueNames: [String] = Array(Set(names)) - return String(format: NSLocalizedString("Highlighted %@", bundle: bundle, comment: "Label to indicate that the user is highlighting 1 user."), locale: locale, uniqueNames.first ?? "") -} diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift index 23b8a4ea..2aab4298 100644 --- a/damus/Views/PostView.swift +++ b/damus/Views/PostView.swift @@ -702,11 +702,6 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post case .highlighting(let draft): break } - - // include pubkeys - tags += pubkeys.map { pk in - ["p", pk.hex()] - } // append additional tags tags += uploadedMedias.compactMap { $0.metadata?.to_tag() } @@ -717,9 +712,14 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post if !(content.isEmpty || content.allSatisfy { $0.isWhitespace }) { tags.append(["comment", content]) } + tags += pubkeys.map { pk in + ["p", pk.hex(), "mention"] + } return NostrPost(content: draft.selected_text, kind: .highlight, tags: tags) default: - break + tags += pubkeys.map { pk in + ["p", pk.hex()] + } } return NostrPost(content: content, kind: .text, tags: tags)