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)