diff --git a/damus/Core/Nostr/Id.swift b/damus/Core/Nostr/Id.swift index 7c352d003..4f0958046 100644 --- a/damus/Core/Nostr/Id.swift +++ b/damus/Core/Nostr/Id.swift @@ -143,6 +143,41 @@ struct ReplaceableParam: TagConvertible { var keychar: AsciiCharacter { "d" } } +struct AddressPointer: Hashable { + let kind: UInt32 + let pubkey: Pubkey + let identifier: String + + init(kind: UInt32, pubkey: Pubkey, identifier: String) { + self.kind = kind + self.pubkey = pubkey + self.identifier = identifier + } + + init?(rawValue: String) { + let components = rawValue.split(separator: ":", maxSplits: 2, omittingEmptySubsequences: false) + guard components.count == 3, + let kind = UInt32(components[0]), + let pubkey = Pubkey(hex: String(components[1])) + else { + return nil + } + + self.init(kind: kind, pubkey: pubkey, identifier: String(components[2])) + } + + static func from(tag: TagSequence, allowedKeys: Set) -> AddressPointer? { + guard tag.count >= 2, + let key = tag[0].single_char, + allowedKeys.contains(key) + else { + return nil + } + + return AddressPointer(rawValue: tag[1].string()) + } +} + struct Signature: Codable, Hashable, Equatable { let data: Data diff --git a/damus/Core/Nostr/NostrKind.swift b/damus/Core/Nostr/NostrKind.swift index ffc16a39e..58a415a1e 100644 --- a/damus/Core/Nostr/NostrKind.swift +++ b/damus/Core/Nostr/NostrKind.swift @@ -18,6 +18,7 @@ enum NostrKind: UInt32, Codable { case boost = 6 case like = 7 case chat = 42 + case comment = 1111 case mute_list = 10000 case relay_list = 10002 case interest_list = 10015 diff --git a/damus/Features/Events/Models/LoadableNostrEventView.swift b/damus/Features/Events/Models/LoadableNostrEventView.swift index 2156c7d86..40422f79e 100644 --- a/damus/Features/Events/Models/LoadableNostrEventView.swift +++ b/damus/Features/Events/Models/LoadableNostrEventView.swift @@ -62,7 +62,7 @@ class LoadableNostrEventViewModel: ObservableObject { guard let ev = await self.loadEvent(noteId: note_id) else { return .not_found } guard let known_kind = ev.known_kind else { return .unknown_or_unsupported_kind } switch known_kind { - case .text, .highlight: + case .text, .highlight, .longform: return .loaded(route: Route.Thread(thread: ThreadModel(event: ev, damus_state: damus_state))) case .dm: let dm_model = damus_state.dms.lookup_or_create(ev.pubkey) @@ -74,7 +74,12 @@ class LoadableNostrEventViewModel: ObservableObject { case .zap, .zap_request: guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found } return .loaded(route: Route.Zaps(target: zap.target)) - case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list, .interest_list, .contact_card: + case .comment: + if let target = commentTarget(ev: ev) { + return await self.executeLoadingLogic(note_reference: target) + } + return .unknown_or_unsupported_kind + case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list, .interest_list, .contact_card: return .unknown_or_unsupported_kind } case .naddr(let naddr): @@ -82,6 +87,32 @@ class LoadableNostrEventViewModel: ObservableObject { return .loaded(route: Route.Thread(thread: ThreadModel(event: event, damus_state: damus_state))) } } + + private func commentTarget(ev: NostrEvent) -> NoteReference? { + guard ev.known_kind == .comment else { + return nil + } + + if let scope = ev.nip22_comment_scope() { + if let parent = scope.parent, let naddr = pointerToNaddr(parent) { + return .naddr(naddr) + } + + if let root = scope.root, let naddr = pointerToNaddr(root) { + return .naddr(naddr) + } + } + + if let parent = damus_state.events.parent_events(event: ev, keypair: damus_state.keypair).last { + return .note_id(parent.id) + } + + return nil + } + + private func pointerToNaddr(_ pointer: AddressPointer) -> NAddr? { + NAddr(identifier: pointer.identifier, author: pointer.pubkey, relays: [], kind: pointer.kind) + } enum ThreadModelLoadingState { case loading diff --git a/damus/Features/Posting/Views/PostView.swift b/damus/Features/Posting/Views/PostView.swift index 18fe208b8..2807fa078 100644 --- a/damus/Features/Posting/Views/PostView.swift +++ b/damus/Features/Posting/Views/PostView.swift @@ -829,6 +829,45 @@ func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair, relayURL: Relay return tags } +private func buildTag(_ key: String, _ components: String?...) -> [String] { + var tag = [key] + for component in components { + guard let component, !component.isEmpty else { continue } + tag.append(component) + } + return tag +} + +func nip22_comment_tags(replying_to: NostrEvent, state: DamusState) -> [[String]]? { + guard replying_to.known_kind == .longform else { + return nil + } + + guard let identifier = replying_to.referenced_params.first?.param.string(), !identifier.isEmpty else { + return nil + } + + let address = "\(replying_to.kind):\(replying_to.pubkey.hex()):\(identifier)" + let relay = state.nostrNetwork.relaysForEvent(event: replying_to).first?.absoluteString + let rootKindString = "\(replying_to.kind)" + let rootPubkey = replying_to.pubkey.hex() + let rootId = replying_to.id.hex() + + var tags: [[String]] = [] + + tags.append(buildTag("A", address, relay)) + tags.append(["K", rootKindString]) + tags.append(buildTag("P", rootPubkey, relay)) + + // For top-level comments, parent equals root + tags.append(buildTag("a", address, relay)) + tags.append(buildTag("e", rootId, relay, rootPubkey)) + tags.append(["k", rootKindString]) + tags.append(buildTag("p", rootPubkey, relay)) + + return tags +} + func build_post(state: DamusState, action: PostAction, draft: DraftArtifacts) -> NostrPost { return build_post( state: state, @@ -912,11 +951,17 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction, } var tags: [[String]] = [] + var postKind: NostrKind = .text switch action { case .replying_to(let replying_to): // start off with the reply tags - tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair, relayURL: state.nostrNetwork.relaysForEvent(event: replying_to).first) + if let nip22Tags = nip22_comment_tags(replying_to: replying_to, state: state) { + tags = nip22Tags + postKind = .comment + } else { + tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair, relayURL: state.nostrNetwork.relaysForEvent(event: replying_to).first) + } case .quoting(let ev): let relay_urls = state.nostrNetwork.relaysForEvent(event: ev) @@ -953,7 +998,7 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction, } } - return NostrPost(content: content.trimmingCharacters(in: .whitespacesAndNewlines), kind: .text, tags: tags) + return NostrPost(content: content.trimmingCharacters(in: .whitespacesAndNewlines), kind: postKind, tags: tags) } func isSupportedVideo(url: URL?) -> Bool { @@ -974,4 +1019,3 @@ func isSupportedImage(url: URL) -> Bool { let supportedTypes = ["jpg", "png", "gif"] return supportedTypes.contains(fileExtension) } - diff --git a/damus/Features/Profile/Models/ProfileModel.swift b/damus/Features/Profile/Models/ProfileModel.swift index 0bfb55199..972e5f031 100644 --- a/damus/Features/Profile/Models/ProfileModel.swift +++ b/damus/Features/Profile/Models/ProfileModel.swift @@ -75,7 +75,7 @@ class ProfileModel: ObservableObject, Equatable { } func subscribe() { - var text_filter = NostrFilter(kinds: [.text, .longform, .highlight]) + var text_filter = NostrFilter(kinds: [.text, .longform, .highlight, .comment]) var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost]) var relay_list_filter = NostrFilter(kinds: [.relay_list], authors: [pubkey]) @@ -98,7 +98,7 @@ class ProfileModel: ObservableObject, Equatable { return } - let conversation_kinds: [NostrKind] = [.text, .longform, .highlight] + let conversation_kinds: [NostrKind] = [.text, .longform, .highlight, .comment] let limit: UInt32 = 500 let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey]) let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey]) diff --git a/damus/Features/Timeline/Models/HomeModel.swift b/damus/Features/Timeline/Models/HomeModel.swift index 245a3d930..8f02eb150 100644 --- a/damus/Features/Timeline/Models/HomeModel.swift +++ b/damus/Features/Timeline/Models/HomeModel.swift @@ -199,7 +199,7 @@ class HomeModel: ContactsDelegate { } switch kind { - case .chat, .longform, .text, .highlight: + case .chat, .longform, .text, .highlight, .comment: handle_text_event(sub_id: sub_id, ev) case .contacts: handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev) @@ -637,7 +637,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, .highlight + .text, .longform, .boost, .highlight, .comment ] if !damus_state.settings.onlyzaps_mode { home_filter_kinds.append(.like) @@ -1214,4 +1214,3 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: } } } -