damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit a64f898df7fbdc4901f634d183c31585b7dd0e7f
parent dd29e871465332828ddeed59b151177166f7601e
Author: Grimless <kyle@kyleroucis.com>
Date:   Mon, 21 Aug 2023 17:17:21 -0400

Move the Block helper type to its own file, collapse the various standalone functions for parsing block data, and refactor consumers to initialize a Block with given data and access its members as needed.

Closes: https://github.com/damus-io/damus/pull/1528
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/ContentParsing.swift | 2+-
Mdamus/Models/Mentions.swift | 227++-----------------------------------------------------------------------------
Adamus/Types/Block.swift | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Util/Zap.swift | 2+-
Mdamus/Views/DMChatView.swift | 4+++-
Mdamus/Views/NoteContentView.swift | 10+++++++++-
Mnostrdb/NdbNote.swift | 10+++++++++-
8 files changed, 244 insertions(+), 229 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -404,6 +404,7 @@ 643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; }; 647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; }; 64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; }; + 7527271E2A93FF0100214108 /* Block.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7527271D2A93FF0100214108 /* Block.swift */; }; 7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; }; 7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; }; 7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */; }; @@ -1078,6 +1079,7 @@ 643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; }; 647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; }; 64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; }; + 7527271D2A93FF0100214108 /* Block.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Block.swift; sourceTree = "<group>"; }; 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSVG.swift; sourceTree = "<group>"; }; 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = "<group>"; }; 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.swift"; sourceTree = "<group>"; }; @@ -1953,6 +1955,7 @@ isa = PBXGroup; children = ( 4CC14FED2A73FCBB007AEB17 /* Ids */, + 7527271D2A93FF0100214108 /* Block.swift */, ); path = Types; sourceTree = "<group>"; @@ -2795,6 +2798,7 @@ 7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */, 4CA352A82A76B37E003BB08B /* NewMutesNotify.swift in Sources */, 4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */, + 7527271E2A93FF0100214108 /* Block.swift in Sources */, 4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */, 4C12536C2A76D4B00004F4B8 /* RepostedNotify.swift in Sources */, 4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */, diff --git a/damus/ContentParsing.swift b/damus/ContentParsing.swift @@ -27,7 +27,7 @@ func parsed_blocks_finish(bs: inout note_blocks, tags: TagsSequence?) -> Blocks while (i < bs.num_blocks) { let block = bs.blocks[i] - if let converted = convert_block(block, tags: tags) { + if let converted = Block(block, tags: tags) { out.append(converted) } diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift @@ -123,147 +123,11 @@ struct LightningInvoice<T> { } } -enum Block: Equatable { - static func == (lhs: Block, rhs: Block) -> Bool { - switch (lhs, rhs) { - case (.text(let a), .text(let b)): - return a == b - case (.mention(let a), .mention(let b)): - return a == b - case (.hashtag(let a), .hashtag(let b)): - return a == b - case (.url(let a), .url(let b)): - return a == b - case (.invoice(let a), .invoice(let b)): - return a.string == b.string - case (_, _): - return false - } - } - - case text(String) - case mention(Mention<MentionRef>) - case hashtag(String) - case url(URL) - case invoice(Invoice) - case relay(String) - - var is_invoice: Invoice? { - if case .invoice(let invoice) = self { - return invoice - } - return nil - } - - var is_hashtag: String? { - if case .hashtag(let htag) = self { - return htag - } - return nil - } - - var is_url: URL? { - if case .url(let url) = self { - return url - } - - return nil - } - - var is_text: String? { - if case .text(let txt) = self { - return txt - } - return nil - } - - var is_note_mention: Bool { - if case .mention(let mention) = self, - case .note = mention.ref { - return true - } - return false - } - - var is_mention: Mention<MentionRef>? { - if case .mention(let m) = self { - return m - } - return nil - } -} - -func render_blocks(blocks: [Block]) -> String { - return blocks.reduce("") { str, block in - switch block { - case .mention(let m): - if let idx = m.index { - return str + "#[\(idx)]" - } - - switch m.ref { - case .pubkey(let pk): return str + "nostr:\(pk.npub)" - case .note(let note_id): return str + "nostr:\(note_id.bech32)" - } - case .relay(let relay): - return str + relay - case .text(let txt): - return str + txt - case .hashtag(let htag): - return str + "#" + htag - case .url(let url): - return str + url.absoluteString - case .invoice(let inv): - return str + inv.string - } - } -} - struct Blocks: Equatable { let words: Int let blocks: [Block] } -func strblock_to_string(_ s: str_block_t) -> String? { - let len = s.end - s.start - let bytes = Data(bytes: s.start, count: len) - return String(bytes: bytes, encoding: .utf8) -} - -func convert_block(_ b: block_t, tags: TagsSequence?) -> Block? { - if b.type == BLOCK_HASHTAG { - guard let str = strblock_to_string(b.block.str) else { - return nil - } - return .hashtag(str) - } else if b.type == BLOCK_TEXT { - guard let str = strblock_to_string(b.block.str) else { - return nil - } - return .text(str) - } else if b.type == BLOCK_MENTION_INDEX { - return convert_mention_index_block(ind: Int(b.block.mention_index), tags: tags) - } else if b.type == BLOCK_URL { - return convert_url_block(b.block.str) - } else if b.type == BLOCK_INVOICE { - return convert_invoice_block(b.block.invoice) - } else if b.type == BLOCK_MENTION_BECH32 { - return convert_mention_bech32_block(b.block.mention_bech32) - } - - return nil -} - -func convert_url_block(_ b: str_block) -> Block? { - guard let str = strblock_to_string(b) else { - return nil - } - guard let url = URL(string: str) else { - return .text(str) - } - return .url(url) -} - func maybe_pointee<T>(_ p: UnsafeMutablePointer<T>!) -> T? { guard p != nil else { return nil @@ -326,75 +190,6 @@ func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String { return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats) } -func convert_invoice_block(_ b: invoice_block) -> Block? { - guard let invstr = strblock_to_string(b.invstr) else { - return nil - } - - guard var b11 = maybe_pointee(b.bolt11) else { - return nil - } - - guard let description = convert_invoice_description(b11: b11) else { - return nil - } - - let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any - let payment_hash = Data(bytes: &b11.payment_hash, count: 32) - let created_at = b11.timestamp - - tal_free(b.bolt11) - return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at)) -} - -func convert_mention_bech32_block(_ b: mention_bech32_block) -> Block? -{ - switch b.bech32.type { - case NOSTR_BECH32_NOTE: - let note = b.bech32.data.note; - let note_id = NoteId(Data(bytes: note.event_id, count: 32)) - return .mention(.any(.note(note_id))) - - case NOSTR_BECH32_NEVENT: - let nevent = b.bech32.data.nevent; - let note_id = NoteId(Data(bytes: nevent.event_id, count: 32)) - return .mention(.any(.note(note_id))) - - case NOSTR_BECH32_NPUB: - let npub = b.bech32.data.npub - let pubkey = Pubkey(Data(bytes: npub.pubkey, count: 32)) - return .mention(.any(.pubkey(pubkey))) - - case NOSTR_BECH32_NSEC: - let nsec = b.bech32.data.nsec - let privkey = Privkey(Data(bytes: nsec.nsec, count: 32)) - guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil } - return .mention(.any(.pubkey(pubkey))) - - case NOSTR_BECH32_NPROFILE: - let nprofile = b.bech32.data.nprofile - let pubkey = Pubkey(Data(bytes: nprofile.pubkey, count: 32)) - return .mention(.any(.pubkey(pubkey))) - - case NOSTR_BECH32_NRELAY: - let nrelay = b.bech32.data.nrelay - guard let relay_str = strblock_to_string(nrelay.relay) else { - return nil - } - return .relay(relay_str) - - case NOSTR_BECH32_NADDR: - // TODO: wtf do I do with this - guard let naddr = strblock_to_string(b.str) else { - return nil - } - return .text("nostr:" + naddr) - - default: - return nil - } -} - func convert_invoice_description(b11: bolt11) -> InvoiceDescription? { if let desc = b11.description { return .description(String(cString: desc)) @@ -407,24 +202,6 @@ func convert_invoice_description(b11: bolt11) -> InvoiceDescription? { return nil } -func convert_mention_index_block(ind: Int, tags: TagsSequence?) -> Block? -{ - guard let tags, - ind >= 0, - ind + 1 <= tags.count - else { - return .text("#[\(ind)]") - } - - let tag = tags[ind] - - guard let mention = MentionRef.from_tag(tag: tag) else { - return .text("#[\(ind)]") - } - - return .mention(.any(mention, index: ind)) -} - func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? { var i: Int = 0 for tag in tags { @@ -474,7 +251,9 @@ func post_to_event(post: NostrPost, keypair: FullKeypair) -> NostrEvent? { let tags = post.references.map({ r in r.tag }) + post.tags let post_blocks = parse_post_blocks(content: post.content) let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags) - let content = render_blocks(blocks: post_tags.blocks) + let content = post_tags.blocks + .map(\.asString) + .joined(separator: "") return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: post.kind.rawValue, tags: post_tags.tags) } diff --git a/damus/Types/Block.swift b/damus/Types/Block.swift @@ -0,0 +1,214 @@ +// +// Block.swift +// damus +// +// Created by Kyle Roucis on 2023-08-21. +// + +import Foundation + + +fileprivate extension String { + /// Failable initializer to build a Swift.String from a C-backed `str_block_t`. + init?(_ s: str_block_t) { + let len = s.end - s.start + let bytes = Data(bytes: s.start, count: len) + self.init(bytes: bytes, encoding: .utf8) + } +} + +/// Represents a block of data stored by the NOSTR protocol. This can be +/// simple text, a hashtag, a url, a relay reference, a mention ref and +/// potentially more in the future. +enum Block: Equatable { + static func == (lhs: Block, rhs: Block) -> Bool { + switch (lhs, rhs) { + case (.text(let a), .text(let b)): + return a == b + case (.mention(let a), .mention(let b)): + return a == b + case (.hashtag(let a), .hashtag(let b)): + return a == b + case (.url(let a), .url(let b)): + return a == b + case (.invoice(let a), .invoice(let b)): + return a.string == b.string + case (_, _): + return false + } + } + + case text(String) + case mention(Mention<MentionRef>) + case hashtag(String) + case url(URL) + case invoice(Invoice) + case relay(String) +} +extension Block { + /// Failable initializer for the C-backed type `block_t`. This initializer will inspect + /// the underlying block type and build the appropriate enum value as needed. + init?(_ block: block_t, tags: TagsSequence? = nil) { + switch block.type { + case BLOCK_HASHTAG: + guard let str = String(block.block.str) else { + return nil + } + self = .hashtag(str) + case BLOCK_TEXT: + guard let str = String(block.block.str) else { + return nil + } + self = .text(str) + case BLOCK_MENTION_INDEX: + guard let b = Block(index: Int(block.block.mention_index), tags: tags) else { + return nil + } + self = b + case BLOCK_URL: + guard let b = Block(block.block.str) else { + return nil + } + self = b + case BLOCK_INVOICE: + guard let b = Block(invoice: block.block.invoice) else { + return nil + } + self = b + case BLOCK_MENTION_BECH32: + guard let b = Block(bech32: block.block.mention_bech32) else { + return nil + } + self = b + default: + return nil + } + } +} +fileprivate extension Block { + /// Failable initializer for the C-backed type `str_block_t`. + init?(_ b: str_block_t) { + guard let str = String(b) else { + return nil + } + + if let url = URL(string: str) { + self = .url(url) + } + else { + self = .text(str) + } + } +} +fileprivate extension Block { + /// Failable initializer for a block index and a tag sequence. + init?(index: Int, tags: TagsSequence? = nil) { + guard let tags, + index >= 0, + index + 1 <= tags.count + else { + self = .text("#[\(index)]") + return + } + + let tag = tags[index] + + if let mention = MentionRef.from_tag(tag: tag) { + self = .mention(.any(mention, index: index)) + } + else { + self = .text("#[\(index)]") + } + } +} +fileprivate extension Block { + /// Failable initializer for the C-backed type `invoice_block_t`. + init?(invoice: invoice_block_t) { + guard let invstr = String(invoice.invstr) else { + return nil + } + + guard var b11 = maybe_pointee(invoice.bolt11) else { + return nil + } + + guard let description = convert_invoice_description(b11: b11) else { + return nil + } + + let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any + let payment_hash = Data(bytes: &b11.payment_hash, count: 32) + let created_at = b11.timestamp + + tal_free(invoice.bolt11) + self = .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at)) + } +} +fileprivate extension Block { + /// Failable initializer for the C-backed type `mention_bech32_block_t`. This initializer will inspect the + /// bech32 type code and build the appropriate enum type. + init?(bech32 b: mention_bech32_block_t) { + switch b.bech32.type { + case NOSTR_BECH32_NOTE: + let note = b.bech32.data.note; + let note_id = NoteId(Data(bytes: note.event_id, count: 32)) + self = .mention(.any(.note(note_id))) + case NOSTR_BECH32_NEVENT: + let nevent = b.bech32.data.nevent; + let note_id = NoteId(Data(bytes: nevent.event_id, count: 32)) + self = .mention(.any(.note(note_id))) + case NOSTR_BECH32_NPUB: + let npub = b.bech32.data.npub + let pubkey = Pubkey(Data(bytes: npub.pubkey, count: 32)) + self = .mention(.any(.pubkey(pubkey))) + case NOSTR_BECH32_NSEC: + let nsec = b.bech32.data.nsec + let privkey = Privkey(Data(bytes: nsec.nsec, count: 32)) + guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil } + self = .mention(.any(.pubkey(pubkey))) + case NOSTR_BECH32_NPROFILE: + let nprofile = b.bech32.data.nprofile + let pubkey = Pubkey(Data(bytes: nprofile.pubkey, count: 32)) + self = .mention(.any(.pubkey(pubkey))) + case NOSTR_BECH32_NRELAY: + let nrelay = b.bech32.data.nrelay + guard let relay_str = String(nrelay.relay) else { + return nil + } + self = .relay(relay_str) + case NOSTR_BECH32_NADDR: + // TODO: wtf do I do with this + guard let naddr = String(b.str) else { + return nil + } + self = .text("nostr:" + naddr) + default: + return nil + } + } +} +extension Block { + var asString: String { + switch self { + case .mention(let m): + if let idx = m.index { + return "#[\(idx)]" + } + + switch m.ref { + case .pubkey(let pk): return "nostr:\(pk.npub)" + case .note(let note_id): return "nostr:\(note_id.bech32)" + } + case .relay(let relay): + return relay + case .text(let txt): + return txt + case .hashtag(let htag): + return "#" + htag + case .url(let url): + return url.absoluteString + case .invoice(let inv): + return inv.string + } + } +} diff --git a/damus/Util/Zap.swift b/damus/Util/Zap.swift @@ -393,7 +393,7 @@ func decode_bolt11(_ s: String) -> Invoice? { let block = bs.blocks[0] - guard let converted = convert_block(block, tags: nil) else { + guard let converted = Block(block) else { blocks_free(&bs) return nil } diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift @@ -130,7 +130,9 @@ struct DMChatView: View, KeyboardReadable { func send_message() { let tags = [["p", pubkey.hex()]] let post_blocks = parse_post_blocks(content: dms.draft) - let content = render_blocks(blocks: post_blocks) + let content = post_blocks + .map(\.asString) + .joined(separator: "") guard let dm = create_dm(content, to_pk: pubkey, tags: tags, keypair: damus_state.keypair) else { print("error creating dm") diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -445,7 +445,15 @@ func render_blocks(blocks bs: Blocks, profiles: Profiles) -> NoteArtifactsSepara let blocks = bs.blocks let one_note_ref = blocks - .filter({ $0.is_note_mention }) + .filter({ + if case .mention(let mention) = $0, + case .note = mention.ref { + return true + } + else { + return false + } + }) .count == 1 var ind: Int = -1 diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift @@ -412,7 +412,15 @@ extension NdbNote { // Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in // and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer. let originalBlocks = self.blocks(keypair).blocks - let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ") + let originalOnlyText = originalBlocks.compactMap { + if case .text(let txt) = $0 { + return txt + } + else { + return nil + } + } + .joined(separator: " ") // Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate. let languageRecognizer = NLLanguageRecognizer()