Private Zaps
This adds private zaps, which have messages and authors encrypted to the target. Keys are deterministically generated so that both the receiver and sender can decrypt. Changelog-Added: Private Zaps
This commit is contained in:
@@ -181,13 +181,12 @@ struct DMChatView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
enum EncEncoding {
|
||||
case base64
|
||||
case bech32
|
||||
}
|
||||
|
||||
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
|
||||
{
|
||||
guard let privkey = keypair.privkey else {
|
||||
return nil
|
||||
}
|
||||
|
||||
func encrypt_message(message: String, privkey: String, to_pk: String, encoding: EncEncoding = .base64) -> String? {
|
||||
let iv = random_bytes(count: 16).bytes
|
||||
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
|
||||
return nil
|
||||
@@ -196,7 +195,26 @@ func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keyp
|
||||
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
|
||||
return nil
|
||||
}
|
||||
let enc_content = encode_dm_base64(content: enc_message.bytes, iv: iv)
|
||||
|
||||
switch encoding {
|
||||
case .base64:
|
||||
return encode_dm_base64(content: enc_message.bytes, iv: iv)
|
||||
case .bech32:
|
||||
return encode_dm_bech32(content: enc_message.bytes, iv: iv)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
|
||||
{
|
||||
guard let privkey = keypair.privkey else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let created = created_at ?? Int64(Date().timeIntervalSince1970)
|
||||
let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags, createdAt: created)
|
||||
|
||||
|
||||
@@ -29,29 +29,29 @@ func eventviewsize_to_font(_ size: EventViewKind) -> Font {
|
||||
|
||||
struct EventView: View {
|
||||
let event: NostrEvent
|
||||
let has_action_bar: Bool
|
||||
let options: EventViewOptions
|
||||
let damus: DamusState
|
||||
let pubkey: String
|
||||
|
||||
@EnvironmentObject var action_bar: ActionBarModel
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, has_action_bar: Bool) {
|
||||
init(damus: DamusState, event: NostrEvent, options: EventViewOptions) {
|
||||
self.event = event
|
||||
self.has_action_bar = has_action_bar
|
||||
self.options = options
|
||||
self.damus = damus
|
||||
self.pubkey = event.pubkey
|
||||
}
|
||||
|
||||
init(damus: DamusState, event: NostrEvent) {
|
||||
self.event = event
|
||||
self.has_action_bar = false
|
||||
self.options = []
|
||||
self.damus = damus
|
||||
self.pubkey = event.pubkey
|
||||
}
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, pubkey: String) {
|
||||
self.event = event
|
||||
self.has_action_bar = false
|
||||
self.options = [.no_action_bar]
|
||||
self.damus = damus
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
@@ -68,7 +68,7 @@ struct EventView: View {
|
||||
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, has_action_bar: has_action_bar, booster_pubkey: event.pubkey)
|
||||
TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, options: options)
|
||||
.padding([.top], 1)
|
||||
}
|
||||
} else {
|
||||
@@ -81,7 +81,7 @@ struct EventView: View {
|
||||
EmptyView()
|
||||
}
|
||||
} else {
|
||||
TextEvent(damus: damus, event: event, pubkey: pubkey, has_action_bar: has_action_bar, booster_pubkey: nil)
|
||||
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
|
||||
.padding([.top], 6)
|
||||
}
|
||||
}
|
||||
@@ -176,11 +176,7 @@ struct EventView_Previews: PreviewProvider {
|
||||
EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .big)
|
||||
|
||||
*/
|
||||
EventView(
|
||||
damus: test_damus_state(),
|
||||
event: test_event,
|
||||
has_action_bar: true
|
||||
)
|
||||
EventView( damus: test_damus_state(), event: test_event )
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ struct MutedEventView: View {
|
||||
if selected {
|
||||
SelectedEventView(damus: damus_state, event: event)
|
||||
} else {
|
||||
EventView(damus: damus_state, event: event, has_action_bar: true)
|
||||
EventView(damus: damus_state, event: event)
|
||||
.onTapGesture {
|
||||
nav_target = event.id
|
||||
navigating = true
|
||||
|
||||
@@ -7,12 +7,22 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EventViewOptions: OptionSet {
|
||||
let rawValue: UInt8
|
||||
static let no_action_bar = EventViewOptions(rawValue: 1 << 0)
|
||||
static let no_replying_to = EventViewOptions(rawValue: 1 << 1)
|
||||
static let no_images = EventViewOptions(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
struct TextEvent: View {
|
||||
let damus: DamusState
|
||||
let event: NostrEvent
|
||||
let pubkey: String
|
||||
let has_action_bar: Bool
|
||||
let booster_pubkey: String?
|
||||
let options: EventViewOptions
|
||||
|
||||
var has_action_bar: Bool {
|
||||
!options.contains(.no_action_bar)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
@@ -62,7 +72,7 @@ struct TextEvent: View {
|
||||
|
||||
struct TextEvent_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", has_action_bar: true, booster_pubkey: nil)
|
||||
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", options: [])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,21 +13,44 @@ struct ZapEvent: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
|
||||
.font(.headline)
|
||||
.padding([.top], 2)
|
||||
HStack(alignment: .center) {
|
||||
Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
|
||||
.font(.headline)
|
||||
.padding([.top], 2)
|
||||
|
||||
if zap.private_request != nil {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(Color("DamusGreen"))
|
||||
.help("Only you can see this message and who sent it.")
|
||||
}
|
||||
}
|
||||
|
||||
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, has_action_bar: false, booster_pubkey: nil)
|
||||
.padding([.top], 1)
|
||||
if let priv = zap.private_request {
|
||||
|
||||
TextEvent(damus: damus, event: priv, pubkey: priv.pubkey, options: [.no_action_bar, .no_replying_to])
|
||||
.padding([.top], 1)
|
||||
} else {
|
||||
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, options: [.no_action_bar, .no_replying_to])
|
||||
.padding([.top], 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
let test_zap_invoice = ZapInvoice(description: .description("description"), amount: 10000, string: "lnbc1", expiry: 1000000, payment_hash: Data(), created_at: 1000000)
|
||||
let test_zap_request_ev = NostrEvent(content: "hi", pubkey: "pk", kind: 9734)
|
||||
let test_zap_request = ZapRequest(ev: test_zap_request_ev)
|
||||
let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: nil)
|
||||
|
||||
let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: test_event)
|
||||
|
||||
struct ZapEvent_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ZapEvent()
|
||||
VStack {
|
||||
ZapEvent(damus: test_damus_state(), zap: test_zap)
|
||||
|
||||
ZapEvent(damus: test_damus_state(), zap: test_private_zap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,19 @@ enum EventGroupType {
|
||||
case zap(ZapGroup)
|
||||
case profile_zap(ZapGroup)
|
||||
|
||||
var zap_group: ZapGroup? {
|
||||
switch self {
|
||||
case .profile_zap(let grp):
|
||||
return grp
|
||||
case .zap(let grp):
|
||||
return grp
|
||||
case .reaction:
|
||||
return nil
|
||||
case .repost:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var events: [NostrEvent] {
|
||||
switch self {
|
||||
case .repost(let grp):
|
||||
@@ -46,10 +59,28 @@ func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
|
||||
return .tagged_in
|
||||
}
|
||||
|
||||
func event_author_name(profiles: Profiles, _ ev: NostrEvent) -> String {
|
||||
let alice_pk = ev.pubkey
|
||||
let alice_prof = profiles.lookup(id: alice_pk)
|
||||
return Profile.displayName(profile: alice_prof, pubkey: alice_pk)
|
||||
func event_author_name(profiles: Profiles, pubkey: String) -> String {
|
||||
let alice_prof = profiles.lookup(id: pubkey)
|
||||
return Profile.displayName(profile: alice_prof, pubkey: pubkey)
|
||||
}
|
||||
|
||||
func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType) -> String {
|
||||
if let zapgrp = group.zap_group {
|
||||
let zap = zapgrp.zaps[ind]
|
||||
|
||||
if let privzap = zap.private_request {
|
||||
return event_author_name(profiles: profiles, pubkey: privzap.pubkey)
|
||||
}
|
||||
|
||||
if zap.is_anon {
|
||||
return "Anonymous"
|
||||
}
|
||||
|
||||
return event_author_name(profiles: profiles, pubkey: zap.request.ev.pubkey)
|
||||
} else {
|
||||
let ev = group.events[ind]
|
||||
return event_author_name(profiles: profiles, pubkey: ev.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,18 +130,16 @@ func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupT
|
||||
case 0:
|
||||
return NSLocalizedString("??", comment: "")
|
||||
case 1:
|
||||
let ev = group.events.first!
|
||||
let profile = profiles.lookup(id: ev.pubkey)
|
||||
let display_name = Profile.displayName(profile: profile, pubkey: ev.pubkey)
|
||||
let display_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
|
||||
|
||||
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, display_name)
|
||||
case 2:
|
||||
let alice_name = event_author_name(profiles: profiles, group.events[0])
|
||||
let bob_name = event_author_name(profiles: profiles, group.events[1])
|
||||
let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
|
||||
let bob_name = event_group_author_name(profiles: profiles, ind: 1, group: group)
|
||||
|
||||
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, alice_name, bob_name)
|
||||
default:
|
||||
let alice_name = event_author_name(profiles: profiles, group.events.first!)
|
||||
let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
|
||||
let count = group.events.count - 1
|
||||
|
||||
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, count, alice_name)
|
||||
|
||||
@@ -52,7 +52,7 @@ struct NotificationItemView: View {
|
||||
|
||||
case .reply(let ev):
|
||||
NavigationLink(destination: BuildThreadV2View(damus: state, event_id: ev.id)) {
|
||||
EventView(damus: state, event: ev, has_action_bar: true)
|
||||
EventView(damus: state, event: ev)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ struct ReplyView: View {
|
||||
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
|
||||
}
|
||||
ScrollView {
|
||||
EventView(damus: damus, event: replying_to, has_action_bar: false)
|
||||
EventView(damus: damus, event: replying_to, options: [.no_action_bar])
|
||||
}
|
||||
PostView(replying_to: replying_to, references: references, damus_state: damus)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ struct InnerTimelineView: View {
|
||||
EmptyTimelineView()
|
||||
} else {
|
||||
ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in
|
||||
EventView(damus: damus, event: ev, has_action_bar: true)
|
||||
EventView(damus: damus, event: ev)
|
||||
.onTapGesture {
|
||||
nav_target = ev
|
||||
navigating = true
|
||||
|
||||
@@ -11,6 +11,7 @@ import Combine
|
||||
enum ZapType {
|
||||
case pub
|
||||
case anon
|
||||
case priv
|
||||
case non_zap
|
||||
}
|
||||
|
||||
@@ -80,11 +81,29 @@ struct CustomizeZapView: View {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
var zap_type_desc: String {
|
||||
switch zap_type {
|
||||
case .pub:
|
||||
return "Everyone on can see that you zapped"
|
||||
case .anon:
|
||||
return "Noone can see that you zapped"
|
||||
case .priv:
|
||||
let pk = event.pubkey
|
||||
let prof = state.profiles.lookup(id: pk)
|
||||
let name = Profile.displayName(profile: prof, pubkey: pk)
|
||||
return String(format: "Only '%@' can see that you zapped them",
|
||||
name)
|
||||
case .non_zap:
|
||||
return "No zaps are sent, only a lightning payment."
|
||||
}
|
||||
}
|
||||
|
||||
var ZapTypePicker: some View {
|
||||
Picker(NSLocalizedString("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send."), selection: $zap_type) {
|
||||
Text("Public", comment: "Picker option to indicate that a zap should be sent publicly and identify the user as who sent it.").tag(ZapType.pub)
|
||||
Text("Anonymous", comment: "Picker option to indicate that a zap should be sent anonymously and not identify the user as who sent it.").tag(ZapType.anon)
|
||||
Text("Non-Zap", comment: "Picker option to indicate that sats should be sent to the user's wallet as a regular Lightning payment, not as a zap.").tag(ZapType.non_zap)
|
||||
Text("Private", comment: "Picker option to indicate that a zap should be sent privately and not identify the user to the public.").tag(ZapType.priv)
|
||||
Text("Anon", comment: "Picker option to indicate that a zap should be sent anonymously and not identify the user as who sent it.").tag(ZapType.anon)
|
||||
Text("None", comment: "Picker option to indicate that sats should be sent to the user's wallet as a regular Lightning payment, not as a zap.").tag(ZapType.non_zap)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
@@ -180,15 +199,17 @@ struct CustomizeZapView: View {
|
||||
}, header: {
|
||||
Text("Comment", comment: "Header text to indicate that the text field below it is a comment that will be used to send as part of a zap to the user.")
|
||||
})
|
||||
|
||||
Section(content: {
|
||||
ZapTypePicker
|
||||
}, header: {
|
||||
Text("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send.")
|
||||
})
|
||||
|
||||
}
|
||||
.dismissKeyboardOnTap()
|
||||
|
||||
Section(content: {
|
||||
ZapTypePicker
|
||||
}, header: {
|
||||
Text("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send.")
|
||||
}, footer: {
|
||||
Text(zap_type_desc)
|
||||
})
|
||||
|
||||
|
||||
if zapping {
|
||||
Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.")
|
||||
|
||||
@@ -21,7 +21,7 @@ struct ZapsView: View {
|
||||
LazyVStack {
|
||||
ForEach(model.zaps, id: \.event.id) { zap in
|
||||
ZapEvent(damus: state, zap: zap)
|
||||
.padding()
|
||||
.padding([.horizontal])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user