damus

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

commit c6ab1de63926ff7f6e29a260e3e7f2473855c9c7
parent dbe1260b542029edd37beb46459050f7312f50dc
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 19 Oct 2022 07:46:05 -0700

Add bolt11 parser and Invoice View

Changelog-Added: Display bolt11 invoice widgets on posts

Diffstat:
Mdamus-c/damus-Bridging-Header.h | 2++
Mdamus-c/damus.c | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mdamus-c/damus.h | 9+++++++++
Mdamus.xcodeproj/project.pbxproj | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Components/InvoiceView.swift | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Components/InvoicesView.swift | 35+++++++++++++++++++++++++++++++++++
Mdamus/Models/EventRef.swift | 2++
Mdamus/Models/Mentions.swift | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mdamus/Views/ChatView.swift | 2+-
Mdamus/Views/DMView.swift | 2+-
Mdamus/Views/EventView.swift | 2+-
Mdamus/Views/NoteContentView.swift | 44+++++++++++++++++++++++++++++---------------
Mdamus/Views/ReplyQuoteView.swift | 2+-
AdamusTests/InvoiceTests.swift | 38++++++++++++++++++++++++++++++++++++++
MdamusTests/ReplyTests.swift | 1-
15 files changed, 411 insertions(+), 41 deletions(-)

diff --git a/damus-c/damus-Bridging-Header.h b/damus-c/damus-Bridging-Header.h @@ -3,3 +3,5 @@ // #include "damus.h" +#include "bolt11.h" +#include "amount.h" diff --git a/damus-c/damus.c b/damus-c/damus.c @@ -6,6 +6,7 @@ // #include "damus.h" +#include "bolt11.h" #include <stdlib.h> #include <string.h> @@ -194,11 +195,60 @@ static int parse_url(struct cursor *cur, struct block *block) { return 1; } +static int parse_invoice(struct cursor *cur, struct block *block) { + const u8 *start, *end; + char *fail; + struct bolt11 *bolt11; + start = cur->p; + + if (!parse_str(cur, "lnbc")) + return 0; + + if (!consume_until_whitespace(cur, 1)) { + cur->p = start; + return 0; + } + + end = cur->p; + + char str[end - start + 1]; + str[end - start] = 0; + memcpy(str, start, end - start); + + if (!(bolt11 = bolt11_decode(NULL, str, &fail))) { + cur->p = start; + return 0; + } + + block->type = BLOCK_INVOICE; + + block->block.invoice.invstr.start = (const char*)start; + block->block.invoice.invstr.end = (const char*)end; + block->block.invoice.bolt11 = bolt11; + + cur->p += end - start; + + return 1; +} + +static int add_text_then_block(struct cursor *cur, struct blocks *blocks, struct block block, u8 **start, u8 *pre_mention) +{ + if (!add_text_block(blocks, *start, pre_mention)) + return 0; + + *start = (u8*)cur->p; + + if (!add_block(blocks, block)) + return 0; + + return 1; +} + int damus_parse_content(struct blocks *blocks, const char *content) { int cp, c; struct cursor cur; struct block block; - const u8 *start, *pre_mention; + u8 *start, *pre_mention; blocks->num_blocks = 0; make_cursor(&cur, (const u8*)content, strlen(content)); @@ -211,24 +261,16 @@ int damus_parse_content(struct blocks *blocks, const char *content) { pre_mention = cur.p; if (cp == -1 || is_whitespace(cp)) { if (c == '#' && (parse_mention(&cur, &block) || parse_hashtag(&cur, &block))) { - if (!add_text_block(blocks, start, pre_mention)) - return 0; - - start = cur.p; - - if (!add_block(blocks, block)) + if (!add_text_then_block(&cur, blocks, block, &start, pre_mention)) return 0; - continue; } else if (c == 'h' && parse_url(&cur, &block)) { - if (!add_text_block(blocks, start, pre_mention)) + if (!add_text_then_block(&cur, blocks, block, &start, pre_mention)) return 0; - - start = cur.p; - - if (!add_block(blocks, block)) + continue; + } else if (c == 'l' && parse_invoice(&cur, &block)) { + if (!add_text_then_block(&cur, blocks, block, &start, pre_mention)) return 0; - continue; } } diff --git a/damus-c/damus.h b/damus-c/damus.h @@ -17,6 +17,7 @@ enum block_type { BLOCK_TEXT = 2, BLOCK_MENTION = 3, BLOCK_URL = 4, + BLOCK_INVOICE = 5, }; typedef struct str_block { @@ -24,10 +25,18 @@ typedef struct str_block { const char *end; } str_block_t; +typedef struct invoice_block { + struct str_block invstr; + union { + struct bolt11 *bolt11; + }; +} invoice_block_t; + typedef struct block { enum block_type type; union { struct str_block str; + struct invoice_block invoice; int mention; } block; } block_t; diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -56,6 +56,22 @@ 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */; }; 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDB281DCE6100B3DE84 /* Liked.swift */; }; 4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */; }; + 4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA63C28FF52D600C48A62 /* bolt11.c */; }; + 4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA64028FF553900C48A62 /* hash_u5.c */; }; + 4C3EA64428FF558100C48A62 /* sha256.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA64328FF558100C48A62 /* sha256.c */; }; + 4C3EA64928FF597700C48A62 /* bech32.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA64828FF597700C48A62 /* bech32.c */; }; + 4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA64B28FF59AC00C48A62 /* bech32_util.c */; }; + 4C3EA64F28FF59F200C48A62 /* tal.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA64E28FF59F200C48A62 /* tal.c */; }; + 4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA65F28FF5E7700C48A62 /* node_id.c */; }; + 4C3EA66528FF5F6800C48A62 /* mem.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA66428FF5F6800C48A62 /* mem.c */; }; + 4C3EA66828FF5F9900C48A62 /* hex.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA66728FF5F9900C48A62 /* hex.c */; }; + 4C3EA66D28FF782800C48A62 /* amount.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA66C28FF782800C48A62 /* amount.c */; }; + 4C3EA67528FF7A5A00C48A62 /* take.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67428FF7A5A00C48A62 /* take.c */; }; + 4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67628FF7A9800C48A62 /* talstr.c */; }; + 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67828FF7ABF00C48A62 /* list.c */; }; + 4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */; }; + 4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */; }; + 4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; }; 4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; }; 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; }; 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; }; @@ -181,6 +197,51 @@ 4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeCounter.swift; sourceTree = "<group>"; }; 4C3BEFDB281DCE6100B3DE84 /* Liked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Liked.swift; sourceTree = "<group>"; }; 4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusState.swift; sourceTree = "<group>"; }; + 4C3EA63B28FF52D600C48A62 /* bolt11.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bolt11.h; sourceTree = "<group>"; }; + 4C3EA63C28FF52D600C48A62 /* bolt11.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = bolt11.c; sourceTree = "<group>"; }; + 4C3EA63E28FF54BD00C48A62 /* short_types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = short_types.h; sourceTree = "<group>"; }; + 4C3EA63F28FF553900C48A62 /* hash_u5.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hash_u5.h; sourceTree = "<group>"; }; + 4C3EA64028FF553900C48A62 /* hash_u5.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hash_u5.c; sourceTree = "<group>"; }; + 4C3EA64228FF558100C48A62 /* sha256.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = sha256.h; sourceTree = "<group>"; }; + 4C3EA64328FF558100C48A62 /* sha256.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = sha256.c; sourceTree = "<group>"; }; + 4C3EA64528FF56D300C48A62 /* config.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = config.h; sourceTree = "<group>"; }; + 4C3EA64628FF570F00C48A62 /* node_id.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = node_id.h; sourceTree = "<group>"; }; + 4C3EA64728FF597700C48A62 /* bech32.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bech32.h; sourceTree = "<group>"; }; + 4C3EA64828FF597700C48A62 /* bech32.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = bech32.c; sourceTree = "<group>"; }; + 4C3EA64A28FF59AC00C48A62 /* bech32_util.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bech32_util.h; sourceTree = "<group>"; }; + 4C3EA64B28FF59AC00C48A62 /* bech32_util.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = bech32_util.c; sourceTree = "<group>"; }; + 4C3EA64D28FF59F200C48A62 /* tal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = tal.h; sourceTree = "<group>"; }; + 4C3EA64E28FF59F200C48A62 /* tal.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = tal.c; sourceTree = "<group>"; }; + 4C3EA65028FF5A5500C48A62 /* list.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = list.h; sourceTree = "<group>"; }; + 4C3EA65328FF5A8600C48A62 /* str.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str.h; sourceTree = "<group>"; }; + 4C3EA65428FF5AAE00C48A62 /* container_of.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = container_of.h; sourceTree = "<group>"; }; + 4C3EA65528FF5AC300C48A62 /* check_type.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = check_type.h; sourceTree = "<group>"; }; + 4C3EA65628FF5B0200C48A62 /* compiler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = compiler.h; sourceTree = "<group>"; }; + 4C3EA65728FF5B1E00C48A62 /* likely.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = likely.h; sourceTree = "<group>"; }; + 4C3EA65828FF5B3700C48A62 /* typesafe_cb.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = typesafe_cb.h; sourceTree = "<group>"; }; + 4C3EA65928FF5B5100C48A62 /* take.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = take.h; sourceTree = "<group>"; }; + 4C3EA65A28FF5BC900C48A62 /* alignof.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = alignof.h; sourceTree = "<group>"; }; + 4C3EA65B28FF5C7E00C48A62 /* str_debug.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str_debug.h; sourceTree = "<group>"; }; + 4C3EA65C28FF5CAF00C48A62 /* endian.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = endian.h; sourceTree = "<group>"; }; + 4C3EA65D28FF5CF300C48A62 /* talstr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = talstr.h; sourceTree = "<group>"; }; + 4C3EA65E28FF5DA400C48A62 /* amount.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = amount.h; sourceTree = "<group>"; }; + 4C3EA65F28FF5E7700C48A62 /* node_id.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = node_id.c; sourceTree = "<group>"; }; + 4C3EA66128FF5EA800C48A62 /* array_size.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = array_size.h; sourceTree = "<group>"; }; + 4C3EA66228FF5EBC00C48A62 /* build_assert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = build_assert.h; sourceTree = "<group>"; }; + 4C3EA66328FF5F6800C48A62 /* mem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = mem.h; sourceTree = "<group>"; }; + 4C3EA66428FF5F6800C48A62 /* mem.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = mem.c; sourceTree = "<group>"; }; + 4C3EA66628FF5F9900C48A62 /* hex.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hex.h; sourceTree = "<group>"; }; + 4C3EA66728FF5F9900C48A62 /* hex.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hex.c; sourceTree = "<group>"; }; + 4C3EA66C28FF782800C48A62 /* amount.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = amount.c; sourceTree = "<group>"; }; + 4C3EA66E28FF787100C48A62 /* overflows.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = overflows.h; sourceTree = "<group>"; }; + 4C3EA67228FF79F600C48A62 /* structeq.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = structeq.h; sourceTree = "<group>"; }; + 4C3EA67328FF7A2600C48A62 /* cppmagic.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cppmagic.h; sourceTree = "<group>"; }; + 4C3EA67428FF7A5A00C48A62 /* take.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = take.c; sourceTree = "<group>"; }; + 4C3EA67628FF7A9800C48A62 /* talstr.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = talstr.c; sourceTree = "<group>"; }; + 4C3EA67828FF7ABF00C48A62 /* list.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = list.c; sourceTree = "<group>"; }; + 4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceTests.swift; sourceTree = "<group>"; }; + 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesView.swift; sourceTree = "<group>"; }; + 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = "<group>"; }; 4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; }; 4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; }; 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; }; @@ -277,6 +338,48 @@ 4C06670828FDE64700038D2A /* damus-Bridging-Header.h */, 4C06670C28FDEAA000038D2A /* utf8.h */, 4C06670D28FDEAA000038D2A /* utf8.c */, + 4C3EA63B28FF52D600C48A62 /* bolt11.h */, + 4C3EA63C28FF52D600C48A62 /* bolt11.c */, + 4C3EA63E28FF54BD00C48A62 /* short_types.h */, + 4C3EA63F28FF553900C48A62 /* hash_u5.h */, + 4C3EA64028FF553900C48A62 /* hash_u5.c */, + 4C3EA64228FF558100C48A62 /* sha256.h */, + 4C3EA64328FF558100C48A62 /* sha256.c */, + 4C3EA64528FF56D300C48A62 /* config.h */, + 4C3EA64628FF570F00C48A62 /* node_id.h */, + 4C3EA64728FF597700C48A62 /* bech32.h */, + 4C3EA64828FF597700C48A62 /* bech32.c */, + 4C3EA64A28FF59AC00C48A62 /* bech32_util.h */, + 4C3EA64B28FF59AC00C48A62 /* bech32_util.c */, + 4C3EA64D28FF59F200C48A62 /* tal.h */, + 4C3EA64E28FF59F200C48A62 /* tal.c */, + 4C3EA65028FF5A5500C48A62 /* list.h */, + 4C3EA65328FF5A8600C48A62 /* str.h */, + 4C3EA65428FF5AAE00C48A62 /* container_of.h */, + 4C3EA65528FF5AC300C48A62 /* check_type.h */, + 4C3EA65628FF5B0200C48A62 /* compiler.h */, + 4C3EA65728FF5B1E00C48A62 /* likely.h */, + 4C3EA65828FF5B3700C48A62 /* typesafe_cb.h */, + 4C3EA65928FF5B5100C48A62 /* take.h */, + 4C3EA65A28FF5BC900C48A62 /* alignof.h */, + 4C3EA65B28FF5C7E00C48A62 /* str_debug.h */, + 4C3EA65C28FF5CAF00C48A62 /* endian.h */, + 4C3EA65D28FF5CF300C48A62 /* talstr.h */, + 4C3EA65E28FF5DA400C48A62 /* amount.h */, + 4C3EA65F28FF5E7700C48A62 /* node_id.c */, + 4C3EA66128FF5EA800C48A62 /* array_size.h */, + 4C3EA66228FF5EBC00C48A62 /* build_assert.h */, + 4C3EA66328FF5F6800C48A62 /* mem.h */, + 4C3EA66428FF5F6800C48A62 /* mem.c */, + 4C3EA66628FF5F9900C48A62 /* hex.h */, + 4C3EA66728FF5F9900C48A62 /* hex.c */, + 4C3EA66C28FF782800C48A62 /* amount.c */, + 4C3EA66E28FF787100C48A62 /* overflows.h */, + 4C3EA67228FF79F600C48A62 /* structeq.h */, + 4C3EA67328FF7A2600C48A62 /* cppmagic.h */, + 4C3EA67428FF7A5A00C48A62 /* take.c */, + 4C3EA67628FF7A9800C48A62 /* talstr.c */, + 4C3EA67828FF7ABF00C48A62 /* list.c */, ); path = "damus-c"; sourceTree = "<group>"; @@ -400,6 +503,8 @@ 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */, 4CD7641A28A1641400B6928F /* EndBlock.swift */, 4C06670528FCB08600038D2A /* ImageCarousel.swift */, + 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */, + 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */, ); path = Components; sourceTree = "<group>"; @@ -459,6 +564,7 @@ 4C363A9F2828A8DD006E126D /* LikeTests.swift */, 4C363A9D2828A822006E126D /* ReplyTests.swift */, 4CE6DEF727F7A08200C66700 /* damusTests.swift */, + 4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */, ); path = damusTests; sourceTree = "<group>"; @@ -626,6 +732,7 @@ 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */, 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */, 4C216F34286F5ACD00040376 /* DMView.swift in Sources */, + 4C3EA64428FF558100C48A62 /* sha256.c in Sources */, 4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */, 4C363AA828297703006E126D /* InsertSort.swift in Sources */, 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */, @@ -633,6 +740,7 @@ 4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */, 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */, 4C75EFB728049D990006080F /* RelayPool.swift in Sources */, + 4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */, 4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */, 4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */, 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, @@ -658,17 +766,21 @@ 4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */, 4C363A9A28283854006E126D /* Reply.swift in Sources */, 4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */, + 4C3EA66828FF5F9900C48A62 /* hex.c in Sources */, 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */, 4C75EFB128049D510006080F /* NostrResponse.swift in Sources */, 4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */, 4C285C8228385570008A31F1 /* CarouselView.swift in Sources */, + 4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */, 4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */, 4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */, 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */, 4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */, 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */, + 4C3EA64928FF597700C48A62 /* bech32.c in Sources */, 4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */, + 4C3EA67528FF7A5A00C48A62 /* take.c in Sources */, 4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */, 4C06670128FC7C5900038D2A /* RelayView.swift in Sources */, 4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */, @@ -676,12 +788,17 @@ 4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */, 4C363A94282704FA006E126D /* Post.swift in Sources */, 4C216F32286E388800040376 /* DMChatView.swift in Sources */, + 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, + 4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */, 4C363A9C282838B9006E126D /* EventRef.swift in Sources */, 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */, + 4C3EA66528FF5F6800C48A62 /* mem.c in Sources */, + 4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */, + 4C3EA64F28FF59F200C48A62 /* tal.c in Sources */, 4C8682872814DE470026224F /* ProfileView.swift in Sources */, 4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, @@ -691,17 +808,21 @@ 4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */, 4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */, 4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */, + 4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */, 4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */, 4C363A962827096D006E126D /* PostBlock.swift in Sources */, 4C5F9116283D855D0052CD1C /* EventsModel.swift in Sources */, 4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */, 4C06670E28FDEAA000038D2A /* utf8.c in Sources */, + 4C3EA66D28FF782800C48A62 /* amount.c in Sources */, 4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */, 4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */, 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */, 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */, 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */, + 4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */, 4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */, + 4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */, 4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */, 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */, 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */, @@ -720,6 +841,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */, 4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */, 4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */, 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */, diff --git a/damus/Components/InvoiceView.swift b/damus/Components/InvoiceView.swift @@ -0,0 +1,53 @@ +// +// InvoiceView.swift +// damus +// +// Created by William Casarin on 2022-10-18. +// + +import SwiftUI + +struct InvoiceView: View { + let invoice: Invoice + + var PayButton: some View { + Button("Pay") { + guard let url = URL(string: "lightning:" + invoice.string) else { + return + } + UIApplication.shared.open(url) + } + .buttonStyle(.bordered) + } + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 20) + .foregroundColor(.secondary.opacity(0.1)) + + VStack(alignment: .trailing, spacing: 12) { + HStack { + Label("", systemImage: "bolt.fill") + .foregroundColor(.orange) + Text("Lightning Invoice") + } + Divider() + Text(invoice.description) + Text("\(invoice.amount / 1000) sats") + .font(.title) + PayButton + .zIndex(5.0) + } + .padding() + } + } +} + +let test_invoice = Invoice(description: "this is a description", amount: 10000, string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119) + +struct InvoiceView_Previews: PreviewProvider { + static var previews: some View { + InvoiceView(invoice: test_invoice) + .frame(width: 200, height: 200) + } +} diff --git a/damus/Components/InvoicesView.swift b/damus/Components/InvoicesView.swift @@ -0,0 +1,35 @@ +// +// InvoicesView.swift +// damus +// +// Created by William Casarin on 2022-10-18. +// + +import SwiftUI + +struct InvoicesView: View { + var invoices: [Invoice] + + @State var open_sheet: Bool = false + @State var current_invoice: Invoice? = nil + + var body: some View { + TabView { + ForEach(invoices, id: \.string) { invoice in + InvoiceView(invoice: invoice) + .tabItem { + Text(invoice.string) + } + .id(invoice.string) + } + } + .frame(height: 200) + .tabViewStyle(PageTabViewStyle()) + } +} + +struct InvoicesView_Previews: PreviewProvider { + static var previews: some View { + InvoicesView(invoices: [Invoice.init(description: "description", amount: 10000, string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)]) + } +} diff --git a/damus/Models/EventRef.swift b/damus/Models/EventRef.swift @@ -82,6 +82,8 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> { return case .url: return + case .invoice: + return } } } diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift @@ -32,11 +32,28 @@ struct IdBlock: Identifiable { let block: Block } +struct Invoice { + let description: String + let amount: Int64 + let string: String + let expiry: UInt64 + let payment_hash: Data + let created_at: UInt64 +} + enum Block { case text(String) case mention(Mention) case hashtag(String) case url(URL) + case invoice(Invoice) + + var is_invoice: Invoice? { + if case .invoice(let invoice) = self { + return invoice + } + return nil + } var is_hashtag: String? { if case .hashtag(let htag) = self { @@ -79,6 +96,8 @@ func render_blocks(blocks: [Block]) -> String { return str + "#" + htag case .url(let url): return str + url.absoluteString + case .invoice(let inv): + return str + inv.string } } } @@ -136,18 +155,53 @@ func convert_block(_ b: block_t, tags: [[String]]) -> Block? { } else if b.type == BLOCK_MENTION { return convert_mention_block(ind: b.block.mention, tags: tags) } else if b.type == BLOCK_URL { - guard let str = strblock_to_string(b.block.str) else { - return nil - } - guard let url = URL(string: str) else { - return .text(str) - } - return .url(url) + return convert_url_block(b.block.str) + } else if b.type == BLOCK_INVOICE { + return convert_invoice_block(b.block.invoice) } 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 + } + return p.pointee +} + +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 + } + + let description = String(cString: b11.description) + guard let msat = maybe_pointee(b11.msat) else { + return nil + } + let amount = Int64(msat.millisatoshis) + let payment_hash = Data(bytes: &b11.payment_hash, count: 32) + let hex = hex_encode(payment_hash) + 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_block(ind: Int32, tags: [[String]]) -> Block? { let ind = Int(ind) diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift @@ -106,7 +106,7 @@ struct ChatView: View { } } - NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, show_images: true, content: event.content) + NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, show_images: true, artifacts: .just_content(event.content)) if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey { let bar = make_actionbar_model(ev: event, damus: damus_state) diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift @@ -21,7 +21,7 @@ struct DMView: View { Spacer() } - NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, show_images: true, content: event.get_content(damus_state.keypair.privkey)) + NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, show_images: true, artifacts: .just_content(event.get_content(damus_state.keypair.privkey))) .foregroundColor(is_ours ? Color.white : Color.primary) .padding(10) .background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15)) diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -129,7 +129,7 @@ struct EventView: View { .frame(maxWidth: .infinity, alignment: .leading) } - NoteContentView(privkey: damus.keypair.privkey, event: event, profiles: damus.profiles, show_images: true, content: content) + NoteContentView(privkey: damus.keypair.privkey, event: event, profiles: damus.profiles, show_images: true, artifacts: .just_content(content)) .frame(maxWidth: .infinity, alignment: .leading) if has_action_bar { diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -7,9 +7,19 @@ import SwiftUI +struct NoteArtifacts { + let content: String + let images: [URL] + let invoices: [Invoice] + + static func just_content(_ content: String) -> NoteArtifacts { + NoteArtifacts(content: content, images: [], invoices: []) + } +} -func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> (String, [URL]) { +func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts { let blocks = ev.blocks(privkey) + var invoices: [Invoice] = [] var img_urls: [URL] = [] let txt = blocks.reduce("") { str, block in switch block { @@ -19,6 +29,9 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) - return str + txt case .hashtag(let htag): return str + hashtag_str(htag) + case .invoice(let invoice): + invoices.append(invoice) + return str case .url(let url): if is_image_url(url) { img_urls.append(url) @@ -27,7 +40,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) - } } - return (txt, img_urls) + return NoteArtifacts(content: txt, images: img_urls, invoices: invoices) } func is_image_url(_ url: URL) -> Bool { @@ -42,21 +55,24 @@ struct NoteContentView: View { let show_images: Bool - @State var content: String - @State var images: [URL] = [] + @State var artifacts: NoteArtifacts func MainContent() -> some View { let md_opts: AttributedString.MarkdownParsingOptions = .init(interpretedSyntax: .inlineOnlyPreservingWhitespace) return VStack(alignment: .leading) { - if let txt = try? AttributedString(markdown: content, options: md_opts) { + if let txt = try? AttributedString(markdown: artifacts.content, options: md_opts) { Text(txt) } else { - Text(content) + Text(artifacts.content) + } + if show_images && artifacts.images.count > 0 { + ImageCarousel(urls: artifacts.images) } - if show_images && images.count > 0 { - ImageCarousel(urls: images) + if artifacts.invoices.count > 0 { + InvoicesView(invoices: artifacts.invoices) + .frame(width: 200) } } } @@ -64,9 +80,7 @@ struct NoteContentView: View { var body: some View { MainContent() .onAppear() { - let (txt, images) = render_note_content(ev: event, profiles: profiles, privkey: privkey) - self.content = txt - self.images = images + self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: privkey) } .onReceive(handle_notify(.profile_updated)) { notif in let profile = notif.object as! ProfileUpdate @@ -75,13 +89,12 @@ struct NoteContentView: View { switch block { case .mention(let m): if m.type == .pubkey && m.ref.ref_id == profile.pubkey { - let (txt, images) = render_note_content(ev: event, profiles: profiles, privkey: privkey) - self.content = txt - self.images = images + self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: privkey) } case .text: return case .hashtag: return case .url: return + case .invoice: return } } } @@ -110,6 +123,7 @@ struct NoteContentView_Previews: PreviewProvider { static var previews: some View { let state = test_damus_state() let content = "hi there https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg" - NoteContentView(privkey: "", event: NostrEvent(content: content, pubkey: "pk"), profiles: state.profiles, show_images: true, content: content) + let artifacts = NoteArtifacts(content: content, images: [], invoices: []) + NoteContentView(privkey: "", event: NostrEvent(content: content, pubkey: "pk"), profiles: state.profiles, show_images: true, artifacts: artifacts) } } diff --git a/damus/Views/ReplyQuoteView.swift b/damus/Views/ReplyQuoteView.swift @@ -31,7 +31,7 @@ struct ReplyQuoteView: View { .foregroundColor(.gray) } - NoteContentView(privkey: privkey, event: event, profiles: profiles, show_images: false, content: event.content) + NoteContentView(privkey: privkey, event: event, profiles: profiles, show_images: false, artifacts: .just_content(event.content)) .font(.callout) .foregroundColor(.accentColor) diff --git a/damusTests/InvoiceTests.swift b/damusTests/InvoiceTests.swift @@ -0,0 +1,38 @@ +// +// InvoiceTests.swift +// damusTests +// +// Created by William Casarin on 2022-10-18. +// + +import XCTest +@testable import damus + +final class InvoiceTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testParseInvoice() throws { + let invstr = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r" + let parsed = parse_mentions(content: invstr, tags: []) + + XCTAssertNotNil(parsed) + XCTAssertEqual(parsed.count, 2) + XCTAssertEqual(parsed[0].is_text, "") + XCTAssertNotNil(parsed[1].is_invoice) + guard let invoice = parsed[1].is_invoice else { + return + } + XCTAssertEqual(invoice.amount, 10000) + XCTAssertEqual(invoice.expiry, 604800) + XCTAssertEqual(invoice.created_at, 1666139119) + XCTAssertEqual(invoice.string, invstr) + } + +} diff --git a/damusTests/ReplyTests.swift b/damusTests/ReplyTests.swift @@ -520,6 +520,5 @@ class ReplyTests: XCTestCase { XCTAssertEqual(parsed[1].is_text, "#[0]") XCTAssertEqual(parsed[2].is_text, " a mention") } - }