Lightning Zaps

Added initial lightning zaps/tipping integration

Changelog-Added: Receive Lightning Zaps
This commit is contained in:
William Casarin
2023-01-16 12:57:31 -08:00
parent 135432e03c
commit 006f8d79e0
31 changed files with 962 additions and 81 deletions

View File

@@ -131,6 +131,12 @@
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88392296F798300DC99E7 /* ReactionsModel.swift */; };
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */; };
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88399297322D200DC99E7 /* DMTests.swift */; };
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */; };
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A72975FC1800DC99E7 /* Zaps.swift */; };
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A9297612FF00DC99E7 /* ZapTests.swift */; };
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; };
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; };
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; };
@@ -140,6 +146,7 @@
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; };
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
@@ -400,6 +407,12 @@
4CB88392296F798300DC99E7 /* ReactionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsModel.swift; sourceTree = "<group>"; };
4CB88395296F7F8B00DC99E7 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
4CB88399297322D200DC99E7 /* DMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMTests.swift; sourceTree = "<group>"; };
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrlPayRequest.swift; sourceTree = "<group>"; };
4CB883A72975FC1800DC99E7 /* Zaps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zaps.swift; sourceTree = "<group>"; };
4CB883A9297612FF00DC99E7 /* ZapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapTests.swift; sourceTree = "<group>"; };
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; };
4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; };
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; };
@@ -409,6 +422,7 @@
4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = "<group>"; };
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
@@ -694,6 +708,7 @@
isa = PBXGroup;
children = (
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
4C3A1D322960DB0500558C0F /* Markdown.swift */,
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
@@ -712,6 +727,9 @@
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */,
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */,
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */,
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */,
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -774,6 +792,7 @@
5C513FB9297F72980072348F /* CustomPicker.swift */,
4CF0ABE22981BC7D00D66079 /* UserView.swift */,
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -842,6 +861,8 @@
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */,
4CB88399297322D200DC99E7 /* DMTests.swift */,
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
4CB883A9297612FF00DC99E7 /* ZapTests.swift */,
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -1081,6 +1102,7 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
@@ -1095,6 +1117,7 @@
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
@@ -1113,6 +1136,7 @@
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */,
@@ -1126,6 +1150,7 @@
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
@@ -1188,6 +1213,7 @@
4C06670E28FDEAA000038D2A /* utf8.c in Sources */,
4C3EA66D28FF782800C48A62 /* amount.c in Sources */,
4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */,
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */,
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
@@ -1231,7 +1257,9 @@
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */,
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,

View File

@@ -31,17 +31,16 @@ func open_with_wallet(wallet: Wallet.Model, invoice: String) {
struct InvoiceView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) private var openURL
let our_pubkey: String
let invoice: Invoice
@State var showing_select_wallet: Bool = false
@EnvironmentObject var user_settings: UserSettingsStore
var PayButton: some View {
Button {
if user_settings.show_wallet_selector {
if should_show_wallet_selector(our_pubkey) {
showing_select_wallet = true
} else {
open_with_wallet(wallet: user_settings.default_wallet.model, invoice: invoice.string)
open_with_wallet(wallet: get_default_wallet(our_pubkey).model, invoice: invoice.string)
}
} label: {
RoundedRectangle(cornerRadius: 20)
@@ -70,7 +69,7 @@ struct InvoiceView: View {
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
}
Divider()
Text(invoice.description)
Text(invoice.description_string)
Text(invoice.amount.amount_sats_str())
.font(.title)
PayButton
@@ -80,16 +79,16 @@ struct InvoiceView: View {
.padding(30)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, invoice: invoice.string).environmentObject(user_settings)
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
}
}
}
let test_invoice = Invoice(description: "this is a description", amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
let test_invoice = Invoice(description: .description("this is a description"), amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
struct InvoiceView_Previews: PreviewProvider {
static var previews: some View {
InvoiceView(invoice: test_invoice)
InvoiceView(our_pubkey: "", invoice: test_invoice)
.frame(width: 200, height: 200)
}
}

View File

@@ -8,6 +8,7 @@
import SwiftUI
struct InvoicesView: View {
let our_pubkey: String
var invoices: [Invoice]
@State var open_sheet: Bool = false
@@ -16,7 +17,7 @@ struct InvoicesView: View {
var body: some View {
TabView {
ForEach(invoices, id: \.string) { invoice in
InvoiceView(invoice: invoice)
InvoiceView(our_pubkey: our_pubkey, invoice: invoice)
.tabItem {
Text(invoice.string)
}
@@ -30,7 +31,7 @@ struct InvoicesView: View {
struct InvoicesView_Previews: PreviewProvider {
static var previews: some View {
InvoicesView(invoices: [Invoice.init(description: "description", amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
.frame(width: 300)
}
}

View File

@@ -0,0 +1,130 @@
//
// ZapButton.swift
// damus
//
// Created by William Casarin on 2023-01-17.
//
import SwiftUI
struct ZapButton: View {
let damus_state: DamusState
let event: NostrEvent
let lnurl: String
@ObservedObject var bar: ActionBarModel
@State var zapping: Bool = false
@State var invoice: String = ""
@State var slider_value: Double = 0.0
@State var slider_visible: Bool = false
@State var showing_select_wallet: Bool = false
func send_zap() {
guard let privkey = damus_state.keypair.privkey else {
return
}
// Only take the first 10 because reasons
let relays = Array(damus_state.pool.descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey)
// TODO: gather comment?
let content = ""
let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target)
zapping = true
Task {
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
if mpayreq == nil {
mpayreq = await fetch_static_payreq(lnurl)
}
guard let payreq = mpayreq else {
// TODO: show error
DispatchQueue.main.async {
zapping = false
}
return
}
DispatchQueue.main.async {
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, amount: 1000000) else {
DispatchQueue.main.async {
zapping = false
}
return
}
DispatchQueue.main.async {
zapping = false
if should_show_wallet_selector(damus_state.pubkey) {
self.invoice = inv
self.showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
}
}
}
//damus_state.pool.send(.event(zapreq))
}
var zap_img: String {
if bar.zapped {
return "bolt.fill"
}
if !zapping {
return "bolt"
}
return "bolt.horizontal.fill"
}
var zap_color: Color? {
if bar.zapped {
return Color.orange
}
if !zapping {
return nil
}
return Color.yellow
}
var body: some View {
ZStack {
EventActionButton(img: zap_img, col: zap_color) {
if bar.zapped {
//notify(.delete, bar.our_tip)
} else if !zapping {
send_zap()
}
}
Text("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")")
.font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
}
}
}
/*
struct ZapButton_Previews: PreviewProvider {
static var previews: some View {
let bar = ActionBarModel.empty()
ZapButton(damus_state: test_damus_state(), event: NostrEvent(content: "hi", pubkey: "pk"), bar: bar)
}
}
*/

View File

@@ -561,7 +561,9 @@ struct ContentView: View {
tips: TipCounter(our_pubkey: pubkey),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache()
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls()
)
home.damus_state = self.damus_state!

View File

@@ -11,30 +11,32 @@ import Foundation
class ActionBarModel: ObservableObject {
@Published var our_like: NostrEvent?
@Published var our_boost: NostrEvent?
@Published var our_tip: NostrEvent?
@Published var our_zap: Zap?
@Published var likes: Int
@Published var boosts: Int
@Published var tips: Int64
@Published var zaps: Int
@Published var zap_total: Int64
static func empty() -> ActionBarModel {
return ActionBarModel(likes: 0, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
}
init(likes: Int, boosts: Int, tips: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_tip: NostrEvent?) {
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?) {
self.likes = likes
self.boosts = boosts
self.tips = tips
self.zaps = zaps
self.zap_total = zap_total
self.our_like = our_like
self.our_boost = our_boost
self.our_tip = our_tip
self.our_zap = our_zap
}
var is_empty: Bool {
return likes == 0 && boosts == 0 && tips == 0
return likes == 0 && boosts == 0 && zaps == 0
}
var tipped: Bool {
return our_tip != nil
var zapped: Bool {
return our_zap != nil
}
var liked: Bool {

View File

@@ -18,6 +18,8 @@ struct DamusState {
let profiles: Profiles
let dms: DirectMessagesModel
let previews: PreviewCache
let zaps: Zaps
let lnurls: LNUrls
var pubkey: String {
return keypair.pubkey
@@ -29,6 +31,6 @@ struct DamusState {
static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache())
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls())
}
}

View File

@@ -112,9 +112,61 @@ class HomeModel: ObservableObject {
handle_channel_create(ev)
case .channel_meta:
handle_channel_meta(ev)
case .zap:
handle_zap_event(ev)
}
}
func handle_zap_event_with_zapper(_ ev: NostrEvent, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
return
}
damus_state.zaps.add_zap(zap: zap)
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
return
}
handle_last_event(ev: ev, timeline: .notifications)
return
}
func handle_zap_event(_ ev: NostrEvent) {
// These are zap notifications
guard let ptag = event_tag(ev, name: "p") else {
return
}
guard ptag == damus_state.pubkey else {
return
}
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: damus_state.pubkey) {
handle_zap_event_with_zapper(ev, zapper: local_zapper)
return
}
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
return
}
guard let lnurl = profile.lnurl else {
return
}
Task {
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
return
}
DispatchQueue.main.async {
self.handle_zap_event_with_zapper(ev, zapper: zapper)
}
}
}
func handle_channel_create(_ ev: NostrEvent) {
guard ev.is_valid else {
return
@@ -317,6 +369,7 @@ class HomeModel: ObservableObject {
NostrKind.chat.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
NostrKind.zap.rawValue,
])
notifications_filter.pubkeys = [damus_state.pubkey]
notifications_filter.limit = 100

View File

@@ -7,6 +7,10 @@
import Foundation
enum CountResult {
case already_counted
case success(Int)
}
class EventCounter {
var counts: [String: Int] = [:]
@@ -14,11 +18,6 @@ class EventCounter {
var our_events: [String: NostrEvent] = [:]
var our_pubkey: String
enum CountResult {
case already_counted
case success(Int)
}
init (our_pubkey: String) {
self.our_pubkey = our_pubkey
}

View File

@@ -32,13 +32,30 @@ struct IdBlock: Identifiable {
let block: Block
}
struct Invoice {
let description: String
let amount: Amount
typealias Invoice = LightningInvoice<Amount>
typealias ZapInvoice = LightningInvoice<Int64>
enum InvoiceDescription {
case description(String)
case description_hash(Data)
}
struct LightningInvoice<T> {
let description: InvoiceDescription
let amount: T
let string: String
let expiry: UInt64
let payment_hash: Data
let created_at: UInt64
var description_string: String {
switch description {
case .description(let string):
return string
case .description_hash(let data):
return ""
}
}
}
enum Block {
@@ -189,20 +206,50 @@ enum Amount: Equatable {
case .any:
return NSLocalizedString("Any", comment: "Any amount of sats")
case .specific(let amt):
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
let sats = NSNumber(value: (Double(amt) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
return format_msats(amt)
}
}
}
func format_msats_abbrev(_ msats: Int64) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.positiveSuffix = "m"
formatter.positivePrefix = ""
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 3
formatter.roundingMode = .down
formatter.roundingIncrement = 0.1
formatter.multiplier = 1
let sats = NSNumber(value: (Double(msats) / 1000.0))
if msats >= 1_000_000*1000 {
formatter.positiveSuffix = "m"
formatter.multiplier = 0.000001
} else if msats >= 1000*1000 {
formatter.positiveSuffix = "k"
formatter.multiplier = 0.001
} else {
return sats.stringValue
}
return formatter.string(from: sats) ?? sats.stringValue
}
func format_msats(_ msat: Int64) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
let sats = NSNumber(value: (Double(msat) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
}
func convert_invoice_block(_ b: invoice_block) -> Block? {
guard let invstr = strblock_to_string(b.invstr) else {
return nil
@@ -212,9 +259,8 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
return nil
}
var description = ""
if b11.description != nil {
description = String(cString: b11.description)
guard let description = convert_invoice_description(b11: b11) else {
return nil
}
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
@@ -225,6 +271,18 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
}
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
if let desc = b11.description {
return .description(String(cString: desc))
}
if var deschash = maybe_pointee(b11.description_hash) {
return .description_hash(Data(bytes: &deschash, count: 32))
}
return nil
}
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
{
let ind = Int(ind)

View File

@@ -8,6 +8,20 @@
import Foundation
import Vault
func should_show_wallet_selector(_ pubkey: String) -> Bool {
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
}
func get_default_wallet(_ pubkey: String) -> Wallet {
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
let default_wallet = Wallet(rawValue: defaultWalletName)
{
return default_wallet
} else {
return .system_default_wallet
}
}
class UserSettingsStore: ObservableObject {
@Published var default_wallet: Wallet {
didSet {
@@ -66,14 +80,10 @@ class UserSettingsStore: ObservableObject {
}
init() {
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
let default_wallet = Wallet(rawValue: defaultWalletName)
{
self.default_wallet = default_wallet
} else {
default_wallet = .system_default_wallet
}
show_wallet_selector = UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
// TODO: pubkey-scoped settings
let pubkey = ""
self.default_wallet = get_default_wallet("")
show_wallet_selector = should_show_wallet_selector("")
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false

View File

@@ -11,6 +11,8 @@ import secp256k1
import secp256k1_implementation
import CryptoKit
enum ValidationResult: Decodable {
case ok
case bad_id
@@ -79,7 +81,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
}
var too_big: Bool {
return self.content.count > 32000
return self.content.count > 16000
}
var should_show_event: Bool {
@@ -371,6 +373,10 @@ func encode_json<T: Encodable>(_ val: T) -> String? {
return (try? encoder.encode(val)).map { String(decoding: $0, as: UTF8.self) }
}
func decode_nostr_event_json(json: String) -> NostrEvent? {
return decode_json(json)
}
func decode_json<T: Decodable>(_ val: String) -> T? {
return try? JSONDecoder().decode(T.self, from: Data(val.utf8))
}
@@ -571,6 +577,26 @@ func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> Nost
return ev
}
func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
switch target {
case .profile(let pk):
return [["p", pk]]
case .note(let note_target):
return [["e", note_target.note_id], ["p", note_target.author]]
}
}
func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget) -> NostrEvent {
var tags = zap_target_to_tags(target)
var relay_tag = ["relays"]
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
tags.append(relay_tag)
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 9734, tags: tags)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: privkey, ev: ev)
return ev
}
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []

View File

@@ -20,4 +20,5 @@ enum NostrKind: Int {
case channel_meta = 41
case chat = 42
case list = 30000
case zap = 9735
}

View File

@@ -12,11 +12,20 @@ import UIKit
class Profiles {
var profiles: [String: TimestampedProfile] = [:]
var validated: [String: NIP05] = [:]
var zappers: [String: String] = [:]
func is_validated(_ pk: String) -> NIP05? {
return validated[pk]
}
func lookup_zapper(pubkey: String) -> String? {
if let zapper = zappers[pubkey] {
return zapper
}
return nil
}
func add(id: String, profile: TimestampedProfile) {
profiles[id] = profile
}

View File

@@ -7,16 +7,16 @@
import Foundation
struct RelayInfo: Codable {
public struct RelayInfo: Codable {
let read: Bool
let write: Bool
static let rw = RelayInfo(read: true, write: true)
}
struct RelayDescriptor: Codable {
let url: URL
let info: RelayInfo
public struct RelayDescriptor: Codable {
public let url: URL
public let info: RelayInfo
}
enum RelayFlags: Int {

View File

@@ -38,6 +38,26 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp:
return true
}
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
var i: Int = 0
for zap in zaps {
// don't insert duplicate events
if new_zap.event.id == zap.event.id {
return false
}
if new_zap.invoice.amount > zap.invoice.amount {
zaps.insert(new_zap, at: i)
return true
}
i += 1
}
zaps.append(new_zap)
return true
}
func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool {
var i: Int = 0

View File

@@ -0,0 +1,24 @@
//
// LNUrl.swift
// damus
//
// Created by William Casarin on 2023-01-16.
//
import Foundation
struct LNUrlPayRequest: Decodable {
let allowsNostr: Bool?
let nostrPubkey: String?
let minSendable: Int64?
let maxSendable: Int64?
let status: String?
let callback: String?
}
struct LNUrlPayResponse: Decodable {
let pr: String
}

20
damus/Util/LNUrls.swift Normal file
View File

@@ -0,0 +1,20 @@
//
// LNUrls.swift
// damus
//
// Created by William Casarin on 2023-01-17.
//
import Foundation
class LNUrls {
var endpoints: [String: LNUrlPayRequest]
init() {
self.endpoints = [:]
}
func lookup(_ id: String) -> LNUrlPayRequest? {
return self.endpoints[id]
}
}

322
damus/Util/Zap.swift Normal file
View File

@@ -0,0 +1,322 @@
//
// Zap.swift
// damus
//
// Created by William Casarin on 2023-01-15.
//
import Foundation
enum ZapSource {
case author(String)
// TODO: anonymous
//case anonymous
}
public struct NoteZapTarget: Equatable {
public let note_id: String
public let author: String
}
public enum ZapTarget: Equatable {
case profile(String)
case note(NoteZapTarget)
public static func note(id: String, author: String) -> ZapTarget {
return .note(NoteZapTarget(note_id: id, author: author))
}
var pubkey: String {
switch self {
case .profile(let pk):
return pk
case .note(let note_target):
return note_target.author
}
}
var id: String {
switch self {
case .note(let note_target):
return note_target.note_id
case .profile(let pk):
return pk
}
}
}
struct ZapRequest {
let ev: NostrEvent
}
struct Zap {
public let event: NostrEvent
public let invoice: ZapInvoice
public let zapper: String /// zap authorizer
public let target: ZapTarget
public let request: ZapRequest
public static func from_zap_event(zap_ev: NostrEvent, zapper: String) -> Zap? {
/// Make sure that we only create a zap event if it is authorized by the profile or event
guard zapper == zap_ev.pubkey else {
return nil
}
guard let bolt11_str = event_tag(zap_ev, name: "bolt11") else {
return nil
}
guard let bolt11 = decode_bolt11(bolt11_str) else {
return nil
}
/// Any amount invoices are not allowed
guard let zap_invoice = invoice_to_zap_invoice(bolt11) else {
return nil
}
// Some endpoints don't have this, let's skip the check for now. We're mostly trusting the zapper anyways
/*
guard let preimage = event_tag(zap_ev, name: "preimage") else {
return nil
}
guard preimage_matches_invoice(preimage, inv: zap_invoice) else {
return nil
}
*/
guard let desc = get_zap_description(zap_ev, inv_desc: zap_invoice.description) else {
return nil
}
guard let zap_req = decode_nostr_event_json(desc) else {
return nil
}
guard let target = determine_zap_target(zap_req) else {
return nil
}
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req))
}
}
/// Fetches the description from either the invoice, or tags, depending on the type of invoice
func get_zap_description(_ ev: NostrEvent, inv_desc: InvoiceDescription) -> String? {
switch inv_desc {
case .description(let string):
return string
case .description_hash(let deschash):
guard let desc = event_tag(ev, name: "description") else {
return nil
}
guard let data = desc.data(using: .utf8) else {
return nil
}
guard sha256(data) == deschash else {
return nil
}
return desc
}
}
func invoice_to_zap_invoice(_ invoice: Invoice) -> ZapInvoice? {
guard case .specific(let amt) = invoice.amount else {
return nil
}
return ZapInvoice(description: invoice.description, amount: amt, string: invoice.string, expiry: invoice.expiry, payment_hash: invoice.payment_hash, created_at: invoice.created_at)
}
func preimage_matches_invoice<T>(_ preimage: String, inv: LightningInvoice<T>) -> Bool {
guard let raw_preimage = hex_decode(preimage) else {
return false
}
let hashed = sha256(Data(raw_preimage))
return inv.payment_hash == hashed
}
func determine_zap_target(_ ev: NostrEvent) -> ZapTarget? {
guard let ptag = event_tag(ev, name: "p") else {
return nil
}
if let etag = event_tag(ev, name: "e") {
return ZapTarget.note(id: etag, author: ptag)
}
return .profile(ptag)
}
func decode_bolt11(_ s: String) -> Invoice? {
var bs = blocks()
bs.num_blocks = 0
blocks_init(&bs)
let bytes = s.utf8CString
let _ = bytes.withUnsafeBufferPointer { p in
damus_parse_content(&bs, p.baseAddress)
}
guard bs.num_blocks == 1 else {
blocks_free(&bs)
return nil
}
let block = bs.blocks[0]
guard let converted = convert_block(block, tags: []) else {
blocks_free(&bs)
return nil
}
guard case .invoice(let invoice) = converted else {
blocks_free(&bs)
return nil
}
blocks_free(&bs)
return invoice
}
func event_tag(_ ev: NostrEvent, name: String) -> String? {
for tag in ev.tags {
if tag.count >= 2 && tag[0] == name {
return tag[1]
}
}
return nil
}
func decode_nostr_event_json(_ desc: String) -> NostrEvent? {
let decoder = JSONDecoder()
guard let dat = desc.data(using: .utf8) else {
return nil
}
guard let ev = try? decoder.decode(NostrEvent.self, from: dat) else {
return nil
}
return ev
}
func decode_zap_request(_ desc: String) -> ZapRequest? {
let decoder = JSONDecoder()
guard let jsonData = desc.data(using: .utf8) else {
return nil
}
guard let jsonArray = try? JSONSerialization.jsonObject(with: jsonData) as? [[Any]] else {
return nil
}
for array in jsonArray {
guard array.count == 2 else {
continue
}
let mkey = array.first.flatMap { $0 as? String }
if let key = mkey, key == "application/nostr" {
guard let dat = try? JSONSerialization.data(withJSONObject: array[1], options: []) else {
return nil
}
guard let zap_req = try? decoder.decode(NostrEvent.self, from: dat) else {
return nil
}
guard zap_req.kind == 9734 else {
return nil
}
/// Ensure the signature on the zap request is correct
guard case .ok = validate_event(ev: zap_req) else {
return nil
}
return ZapRequest(ev: zap_req)
}
}
return nil
}
func fetch_zapper_from_lnurl(_ lnurl: String) async -> String? {
guard let endpoint = await fetch_static_payreq(lnurl) else {
return nil
}
guard let allows = endpoint.allowsNostr, allows else {
return nil
}
guard let key = endpoint.nostrPubkey, key.count == 64 else {
return nil
}
return endpoint.nostrPubkey
}
func decode_lnurl(_ lnurl: String) -> URL? {
guard let decoded = try? bech32_decode(lnurl) else {
return nil
}
guard decoded.hrp == "lnurl" else {
return nil
}
guard let url = URL(string: String(decoding: decoded.data, as: UTF8.self)) else {
return nil
}
return url
}
func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
guard let url = decode_lnurl(lnurl) else {
return nil
}
guard let ret = try? await URLSession.shared.data(from: url) else {
return nil
}
let json_str = String(decoding: ret.0, as: UTF8.self)
guard let endpoint: LNUrlPayRequest = decode_json(json_str) else {
return nil
}
return endpoint
}
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, amount: Int64) async -> String? {
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
return nil
}
let zappable = payreq.allowsNostr ?? false
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
if zappable {
if let json = encode_json(zapreq) {
query.append(URLQueryItem(name: "nostr", value: json))
}
}
base_url.queryItems = query
guard let url = base_url.url else {
return nil
}
print("url \(url)")
guard let ret = try? await URLSession.shared.data(from: url) else {
return nil
}
let json_str = String(decoding: ret.0, as: UTF8.self)
guard let result: LNUrlPayResponse = decode_json(json_str) else {
print("fetch_zap_invoice error: \(json_str)")
return nil
}
return result.pr
}

65
damus/Util/Zaps.swift Normal file
View File

@@ -0,0 +1,65 @@
//
// Zaps.swift
// damus
//
// Created by William Casarin on 2023-01-16.
//
import Foundation
class Zaps {
var zaps: [String: Zap]
let our_pubkey: String
var our_zaps: [String: [Zap]]
var event_counts: [String: Int]
var event_totals: [String: Int64]
init(our_pubkey: String) {
self.zaps = [:]
self.our_pubkey = our_pubkey
self.our_zaps = [:]
self.event_counts = [:]
self.event_totals = [:]
}
func add_zap(zap: Zap) {
if zaps[zap.event.id] != nil {
return
}
self.zaps[zap.event.id] = zap
// record our zaps for an event
if zap.request.ev.pubkey == our_pubkey {
switch zap.target {
case .note(let note_target):
if our_zaps[note_target.note_id] == nil {
our_zaps[note_target.note_id] = [zap]
} else {
let _ = insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
}
case .profile(_):
break
}
}
// don't count tips to self. lame.
guard zap.request.ev.pubkey != zap.target.pubkey else {
return
}
let id = zap.target.id
if event_counts[id] == nil {
event_counts[id] = 0
}
if event_totals[id] == nil {
event_totals[id] = 0
}
event_counts[id] = event_counts[id]! + 1
event_totals[id] = event_totals[id]! + zap.invoice.amount
return
}
}

View File

@@ -22,6 +22,7 @@ struct EventActionBar: View {
let damus_state: DamusState
let event: NostrEvent
let generator = UIImpactFeedbackGenerator(style: .medium)
@State var sheet: ActionBarSheet? = nil
@State var confirm_boost: Bool = false
@State var show_share_sheet: Bool = false
@@ -64,6 +65,12 @@ struct EventActionBar: View {
.foregroundColor(bar.liked ? Color.accentColor : Color.gray)
}
if let lnurl = damus_state.profiles.lookup(id: event.pubkey)?.lnurl {
Spacer()
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, bar: bar)
}
Spacer()
EventActionButton(img: "square.and.arrow.up", col: Color.gray) {
show_share_sheet = true
@@ -155,10 +162,11 @@ struct EventActionBar_Previews: PreviewProvider {
let ds = test_damus_state()
let ev = NostrEvent(content: "hi", pubkey: pk)
let bar = ActionBarModel(likes: 0, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
let likedbar = ActionBarModel(likes: 10, boosts: 10, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
let likedbar_ours = ActionBarModel(likes: 100, boosts: 100, tips: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_tip: nil)
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, tips: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_tip: nil)
let bar = ActionBarModel.empty()
let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_zap: nil)
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, our_like: nil, our_boost: nil, our_zap: nil)
VStack(spacing: 50) {
EventActionBar(damus_state: ds, event: ev, bar: bar)
@@ -168,6 +176,8 @@ struct EventActionBar_Previews: PreviewProvider {
EventActionBar(damus_state: ds, event: ev, bar: likedbar_ours)
EventActionBar(damus_state: ds, event: ev, bar: maxed_bar)
EventActionBar(damus_state: ds, event: ev, bar: zapbar)
}
.padding(20)
}

View File

@@ -28,8 +28,8 @@ struct EventDetailBar: View {
.buttonStyle(PlainButtonStyle())
}
if bar.tips > 0 {
Text("\(Text("\(bar.tips)", comment: "Number of tip payments on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("tips_count", comment: "Part of a larger sentence to describe how many tip payments there are on a post."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many tip payments there are on a post. In source English, the first variable is the number of tip payments, and the second variable is 'Tip' or 'Tips'.")
if bar.zaps > 0 {
Text("\(Text("\(bar.zaps)", comment: "Number of zap payments on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("zaps_count", comment: "Part of a larger sentence to describe how many zap payments there are on a post."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
}
}
}

View File

@@ -96,7 +96,7 @@ struct ChatView: View {
if let ref_id = thread.replies.lookup(event.id) {
if !is_reply_to_prev() {
ReplyQuoteView(privkey: damus_state.keypair.privkey, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews)
ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews)
.frame(maxHeight: expand_reply ? nil : 100)
.environmentObject(thread)
.onTapGesture {
@@ -106,7 +106,7 @@ struct ChatView: View {
}
}
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey), artifacts: .just_content(event.content), size: .normal)
NoteContentView(keypair: damus_state.keypair, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey), artifacts: .just_content(event.content), size: .normal)
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
let bar = make_actionbar_model(ev: event, damus: damus_state)

View File

@@ -23,7 +23,7 @@ struct DMView: View {
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_img, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), size: .normal)
NoteContentView(keypair: damus_state.keypair, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_img, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), size: .normal)
.foregroundColor(is_ours ? Color.white : Color.primary)
.padding(10)
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))

View File

@@ -72,10 +72,26 @@ struct EventView: View {
TextEvent(inner_ev, pubkey: inner_ev.pubkey, booster_pubkey: event.pubkey)
.padding([.top], 1)
}
} else if event.known_kind == .zap {
if let zap = damus.zaps.zaps[event.id] {
VStack(alignment: .leading) {
Text("⚡️ \(format_msats(zap.invoice.amount))")
.font(.headline)
.padding([.top], 2)
TextEvent(zap.request.ev, pubkey: zap.request.ev.pubkey, booster_pubkey: nil)
.padding([.top], 1)
}
} else {
EmptyView()
}
} else {
TextEvent(event, pubkey: pubkey)
.padding([.top], 6)
}
Divider()
.padding([.top], 4)
}
}
@@ -197,17 +213,19 @@ func format_date(_ created_at: Int64) -> String {
func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel {
let likes = damus.likes.counts[ev.id]
let boosts = damus.boosts.counts[ev.id]
let tips = damus.tips.tips[ev.id]
let zaps = damus.zaps.event_counts[ev.id]
let zap_total = damus.zaps.event_totals[ev.id]
let our_like = damus.likes.our_events[ev.id]
let our_boost = damus.boosts.our_events[ev.id]
let our_tip = damus.tips.our_tips[ev.id]
let our_zap = damus.zaps.our_zaps[ev.id]
return ActionBarModel(likes: likes ?? 0,
boosts: boosts ?? 0,
tips: tips ?? 0,
zaps: zaps ?? 0,
zap_total: zap_total ?? 0,
our_like: our_like,
our_boost: our_boost,
our_tip: our_tip
our_zap: our_zap?.first
)
}

View File

@@ -66,7 +66,7 @@ func is_image_url(_ url: URL) -> Bool {
}
struct NoteContentView: View {
let privkey: String?
let keypair: Keypair
let event: NostrEvent
let profiles: Profiles
let previews: PreviewCache
@@ -138,7 +138,7 @@ struct NoteContentView: View {
.cornerRadius(10)
}
if artifacts.invoices.count > 0 {
InvoicesView(invoices: artifacts.invoices)
InvoicesView(our_pubkey: keypair.pubkey, invoices: artifacts.invoices)
}
if let preview = self.preview, show_images {
@@ -157,16 +157,16 @@ struct NoteContentView: View {
var body: some View {
MainContent()
.onAppear() {
self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: privkey)
self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: keypair.privkey)
}
.onReceive(handle_notify(.profile_updated)) { notif in
let profile = notif.object as! ProfileUpdate
let blocks = event.blocks(privkey)
let blocks = event.blocks(keypair.privkey)
for block in blocks {
switch block {
case .mention(let m):
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: privkey)
self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: keypair.privkey)
}
case .text: return
case .hashtag: return

View File

@@ -161,7 +161,7 @@ struct ProfileView: View {
}
.cornerRadius(24)
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, invoice: lnurl)
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl)
.environmentObject(user_settings)
}
}
@@ -409,7 +409,7 @@ struct ProfileView_Previews: PreviewProvider {
func test_damus_state() -> DamusState {
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let damus = DamusState(pool: RelayPool(), keypair: Keypair(pubkey: pubkey, privkey: "privkey"), likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(our_pubkey: pubkey), tips: TipCounter(our_pubkey: pubkey), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: pubkey), previews: PreviewCache())
let damus = DamusState(pool: RelayPool(), keypair: Keypair(pubkey: pubkey, privkey: "privkey"), likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(our_pubkey: pubkey), tips: TipCounter(our_pubkey: pubkey), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: pubkey), previews: PreviewCache(), zaps: Zaps(our_pubkey: pubkey), lnurls: LNUrls())
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io")
let tsprof = TimestampedProfile(profile: prof, timestamp: 0)

View File

@@ -8,7 +8,7 @@
import SwiftUI
struct ReplyQuoteView: View {
let privkey: String?
let keypair: Keypair
let quoter: NostrEvent
let event_id: String
let profiles: Profiles
@@ -32,7 +32,7 @@ struct ReplyQuoteView: View {
.foregroundColor(.gray)
}
NoteContentView(privkey: privkey, event: event, profiles: profiles, previews: previews, show_images: false, artifacts: .just_content(event.content), size: .normal)
NoteContentView(keypair: keypair, event: event, profiles: profiles, previews: previews, show_images: false, artifacts: .just_content(event.content), size: .normal)
.font(.callout)
.foregroundColor(.accentColor)
@@ -59,7 +59,7 @@ struct ReplyQuoteView_Previews: PreviewProvider {
static var previews: some View {
let s = test_damus_state()
let quoter = NostrEvent(content: "a\nb\nc", pubkey: "pubkey")
ReplyQuoteView(privkey: s.keypair.privkey, quoter: quoter, event_id: "pubkey2", profiles: s.profiles, previews: PreviewCache())
ReplyQuoteView(keypair: s.keypair, quoter: quoter, event_id: "pubkey2", profiles: s.profiles, previews: PreviewCache())
.environmentObject(ThreadModel(event: quoter, damus_state: s))
}
}

View File

@@ -9,10 +9,10 @@ import SwiftUI
struct SelectWalletView: View {
@Binding var showingSelectWallet: Bool
let our_pubkey: String
let invoice: String
@Environment(\.openURL) private var openURL
@State var invoice_copied: Bool = false
@EnvironmentObject var user_settings: UserSettingsStore
@State var allWalletModels: [Wallet.Model] = Wallet.allModels
let generator = UIImpactFeedbackGenerator(style: .light)
@@ -38,7 +38,7 @@ struct SelectWalletView: View {
Section(NSLocalizedString("Select a Lightning wallet", comment: "Title of section for selecting a Lightning wallet to pay a Lightning invoice.")) {
List{
Button() {
let wallet_model = user_settings.default_wallet.model
let wallet_model = get_default_wallet(our_pubkey).model
open_with_wallet(wallet: wallet_model, invoice: invoice)
} label: {
HStack {
@@ -73,6 +73,6 @@ struct SelectWalletView_Previews: PreviewProvider {
@State static var invoice: String = ""
static var previews: some View {
SelectWalletView(showingSelectWallet: $show, invoice: "")
SelectWalletView(showingSelectWallet: $show, our_pubkey: "", invoice: "")
}
}

View File

@@ -0,0 +1,37 @@
//
// FormatTests.swift
// damusTests
//
// Created by William Casarin on 2023-01-17.
//
import XCTest
@testable import damus
final class FormatTests: 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 testAbbrevSatsFormat() throws {
XCTAssertEqual(format_msats_abbrev(1_000_000 * 1000), "1m")
XCTAssertEqual(format_msats_abbrev(1_100_000 * 1000), "1.1m")
XCTAssertEqual(format_msats_abbrev(100_000_000 * 1000), "100m")
XCTAssertEqual(format_msats_abbrev(1000 * 1000), "1k")
XCTAssertEqual(format_msats_abbrev(1500 * 1000), "1.5k")
XCTAssertEqual(format_msats_abbrev(1595 * 1000), "1.5k")
XCTAssertEqual(format_msats_abbrev(100 * 1000), "100")
XCTAssertEqual(format_msats_abbrev(0), "0")
XCTAssertEqual(format_msats_abbrev(100_000_000 * 1000), "100m")
XCTAssertEqual(format_msats_abbrev(999 * 1000), "999")
XCTAssertEqual(format_msats_abbrev(999), "0.999")
XCTAssertEqual(format_msats_abbrev(1), "0.001")
XCTAssertEqual(format_msats_abbrev(1000), "1")
}
}

45
damusTests/ZapTests.swift Normal file
View File

@@ -0,0 +1,45 @@
//
// ZapTests.swift
// damusTests
//
// Created by William Casarin on 2023-01-16.
//
import XCTest
@testable import damus
final class ZapTests: 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 testZap() throws {
let zapjson = "eyJpZCI6IjUzNmJlZTllODNjODE4ZTNiODJjMTAxOTM1MTI4YWUyN2EwZDQyOTAwMzlhYWYyNTNlZmU1ZjA5MjMyYzE5NjIiLCJwdWJrZXkiOiI5NjMwZjQ2NGNjYTZhNTE0N2FhOGEzNWYwYmNkZDNjZTQ4NTMyNGU3MzJmZDM5ZTA5MjMzYjFkODQ4MjM4ZjMxIiwiY3JlYXRlZF9hdCI6MTY3NDIwNDUzNSwia2luZCI6OTczNSwidGFncyI6W1sicCIsIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDUiXSxbImJvbHQxMSIsImxuYmMxMHUxcDN1NTR0bnNwNTcyOXF2eG5renRqamtkNTg1eW4wbDg2MzBzMm01eDZsNTZ3eXk0ZWMybnU4eHV6NjI5eHFwcDV2MnE3aHVjNGpwamgwM2Z4OHVqZXQ1Nms3OWd4cXg3bWUycGV2ejZqMms4dDhtNGxnNXZxaHA1eWc1MDU3OGNtdWoyNG1mdDNxcnNybWd3ZjMwa2U3YXY3ZDc3Z2FtZmxkazlrNHNmMzltcXhxeWp3NXFjcXBqcnpqcTJoeWVoNXEzNmx3eDZ6dHd5cmw2dm1tcnZ6NnJ1ZndqZnI4N3lremZuYXR1a200dWRzNHl6YWszc3FxOW1jcXFxcXFxcWxncXFxcTg2cXF5ZzlxeHBxeXNncWFkeWVjdmR6ZjI3MHBkMzZyc2FmbDA3azQ1ZmNqMnN5OGU1djJ0ZW5kNTB2OTU3NnV4cDNkdmp6amV1aHJlODl5cGdjbTkwZDZsbTAwNGszMHlqNGF2NW1jc3M1bnl4NHU5bmVyOWdwcHY2eXF3Il0sWyJkZXNjcmlwdGlvbiIsIntcImlkXCI6XCJiMDkyMTYzNGIxYmI4ZWUzNTg0YmJiZjJlOGQ3OTBhZDk4NTk5ZDhlMDhmODFjNzAwZGRiZTQ4MjAxNTY4Yjk3XCIsXCJwdWJrZXlcIjpcIjdmYTU2ZjVkNjk2MmFiMWUzY2Q0MjRlNzU4YzMwMDJiODY2NWY3YjBkOGRjZWU5ZmU5ZTI4OGQ3NzUxYWMxOTRcIixcImNyZWF0ZWRfYXRcIjoxNjc0MjA0NTMxLFwia2luZFwiOjk3MzQsXCJ0YWdzXCI6W1tcInBcIixcIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDVcIl0sW1wicmVsYXlzXCIsXCJ3c3M6Ly9yZWxheS5zbm9ydC5zb2NpYWxcIixcIndzczovL3JlbGF5LmRhbXVzLmlvXCIsXCJ3c3M6Ly9ub3N0ci1wdWIud2VsbG9yZGVyLm5ldFwiLFwid3NzOi8vbm9zdHIudjBsLmlvXCIsXCJ3c3M6Ly9wcml2YXRlLW5vc3RyLnYwbC5pb1wiLFwid3NzOi8vbm9zdHIuemViZWRlZS5jbG91ZFwiLFwid3NzOi8vcmVsYXkubm9zdHIuaW5mby9cIl1dLFwiY29udGVudFwiOlwiXCIsXCJzaWdcIjpcImQwODQwNGU2MjVmOWM1NjMzYWZhZGQxMWMxMTBiYTg4ZmNkYjRiOWUwOTJiOTg0MGU3NDgyYThkNTM3YjFmYzExODY5MmNmZDEzMWRkODMzNTM2NDc2OWE2NzE3NTRhZDdhYTk3MzEzNjgzYTRhZDdlZmI3NjQ3NmMwNGU1ZjE3XCJ9Il0sWyJwcmVpbWFnZSIsIjNlMDJhM2FmOGM4YmNmMmEzNzUzYzg3ZjMxMTJjNjU2YTIwMTE0ZWUwZTk4ZDgyMTliYzU2ZjVlOGE3MjM1YjMiXV0sImNvbnRlbnQiOiIiLCJzaWciOiIzYWI0NGQwZTIyMjhiYmQ0ZDIzNDFjM2ZhNzQwOTZjZmY2ZjU1Y2ZkYTk5YTVkYWRjY2Y0NWM2NjQ2MzdlMjExNTFiMmY5ZGQwMDQwZjFhMjRlOWY4Njg2NzM4YjE2YmY4MTM0YmRiZTQxYTIxOGM5MTFmN2JiMzFlNTk1NzhkMSJ9Cg=="
guard let json_data = Data(base64Encoded: zapjson) else {
XCTAssert(false)
return
}
let json_str = String(decoding: json_data, as: UTF8.self)
guard let ev = decode_nostr_event_json(json: json_str) else {
XCTAssert(false)
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: "9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31") else {
XCTAssert(false)
return
}
XCTAssertEqual(zap.zapper, "9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31")
XCTAssertEqual(zap.target, ZapTarget.profile("32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"))
}
}