Add bolt11 parser and Invoice View
Changelog-Added: Display bolt11 invoice widgets on posts
This commit is contained in:
53
damus/Components/InvoiceView.swift
Normal file
53
damus/Components/InvoiceView.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
35
damus/Components/InvoicesView.swift
Normal file
35
damus/Components/InvoicesView.swift
Normal file
@@ -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)])
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,8 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
|
||||
return
|
||||
case .url:
|
||||
return
|
||||
case .invoice:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 && images.count > 0 {
|
||||
ImageCarousel(urls: images)
|
||||
if show_images && artifacts.images.count > 0 {
|
||||
ImageCarousel(urls: artifacts.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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user