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:
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")
}
-
}