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