damus

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

commit 370a5feb4e4484f24a8f3f23794f29cb5e29c62b
parent fe3d976cdbff19ec61efba322d5d39769e8e595d
Author: William Casarin <jb55@jb55.com>
Date:   Tue,  9 May 2023 18:50:08 -0700

ui: add Nostr Wallet Connect views

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 24++++++++++++++++++++++++
Mdamus/ContentView.swift | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mdamus/Models/DamusState.swift | 3++-
Mdamus/Views/SideMenuView.swift | 14++++++++++----
Adamus/Views/Wallet/ConnectWalletView.swift | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/Wallet/NWCScannerView.swift | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/Wallet/WalletView.swift | 40++++++++++++++++++++++++++++++++++++++++
7 files changed, 310 insertions(+), 27 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -137,7 +137,11 @@ 4C75EFB92804A2740006080F /* EventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB82804A2740006080F /* EventView.swift */; }; 4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */; }; 4C7D09592A05BEAD00943473 /* KeyboardVisible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */; }; + 4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D095C2A098C5D00943473 /* ConnectWalletView.swift */; }; + 4C7D09602A098C5D00943473 /* WalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D095D2A098C5D00943473 /* WalletView.swift */; }; + 4C7D09622A098D0E00943473 /* WalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09612A098D0E00943473 /* WalletConnect.swift */; }; 4C7D09662A0AE62100943473 /* AlbyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09652A0AE62100943473 /* AlbyButton.swift */; }; + 4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09672A0AE9B200943473 /* NWCScannerView.swift */; }; 4C7D096D2A0AEA0400943473 /* CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096A2A0AEA0400943473 /* CodeScanner.swift */; }; 4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096B2A0AEA0400943473 /* ScannerCoordinator.swift */; }; 4C7D096F2A0AEA0400943473 /* ScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096C2A0AEA0400943473 /* ScannerViewController.swift */; }; @@ -553,7 +557,11 @@ 4C75EFB82804A2740006080F /* EventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventView.swift; sourceTree = "<group>"; }; 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = "<group>"; }; 4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardVisible.swift; sourceTree = "<group>"; }; + 4C7D095C2A098C5D00943473 /* ConnectWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectWalletView.swift; sourceTree = "<group>"; }; + 4C7D095D2A098C5D00943473 /* WalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletView.swift; sourceTree = "<group>"; }; + 4C7D09612A098D0E00943473 /* WalletConnect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnect.swift; sourceTree = "<group>"; }; 4C7D09652A0AE62100943473 /* AlbyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbyButton.swift; sourceTree = "<group>"; }; + 4C7D09672A0AE9B200943473 /* NWCScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWCScannerView.swift; sourceTree = "<group>"; }; 4C7D096A2A0AEA0400943473 /* CodeScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeScanner.swift; sourceTree = "<group>"; }; 4C7D096B2A0AEA0400943473 /* ScannerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerCoordinator.swift; sourceTree = "<group>"; }; 4C7D096C2A0AEA0400943473 /* ScannerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerViewController.swift; sourceTree = "<group>"; }; @@ -924,6 +932,7 @@ isa = PBXGroup; children = ( 4C7D09692A0AEA0400943473 /* CodeScanner */, + 4C7D095A2A098C5C00943473 /* Wallet */, 4C8D1A6D29F31E4100ACDF75 /* Buttons */, 4C1A9A1B29DDCF8B00516EAC /* Settings */, 4CFF8F6129CC9A80008DB934 /* Images */, @@ -1005,6 +1014,16 @@ path = Nostr; sourceTree = "<group>"; }; + 4C7D095A2A098C5C00943473 /* Wallet */ = { + isa = PBXGroup; + children = ( + 4C7D095C2A098C5D00943473 /* ConnectWalletView.swift */, + 4C7D095D2A098C5D00943473 /* WalletView.swift */, + 4C7D09672A0AE9B200943473 /* NWCScannerView.swift */, + ); + path = Wallet; + sourceTree = "<group>"; + }; 4C7D09692A0AEA0400943473 /* CodeScanner */ = { isa = PBXGroup; children = ( @@ -1027,6 +1046,7 @@ 4C7FF7D628233637009601DB /* Util */ = { isa = PBXGroup; children = ( + 4C7D09612A098D0E00943473 /* WalletConnect.swift */, 4C198DF329F88D23004C165C /* Images */, 4C198DEA29F88C6B004C165C /* BlurHash */, 4CE4F0F329D779B5005914DB /* PostBox.swift */, @@ -1622,6 +1642,7 @@ F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */, 4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */, 4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */, + 4C7D09602A098C5D00943473 /* WalletView.swift in Sources */, 4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */, 4C75EFB328049D640006080F /* NostrEvent.swift in Sources */, 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */, @@ -1664,6 +1685,7 @@ 4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */, 4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */, 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, + 4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */, 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */, 7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */, 4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */, @@ -1695,6 +1717,7 @@ 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */, 4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */, 4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */, + 4C7D09622A098D0E00943473 /* WalletConnect.swift in Sources */, 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */, @@ -1710,6 +1733,7 @@ 4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */, 4C42812C298C848200DBF26F /* TranslateView.swift in Sources */, 4C363A9C282838B9006E126D /* EventRef.swift in Sources */, + 4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */, 3AA24802297E3DC20090C62D /* RepostView.swift in Sources */, 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */, 4C3EA66528FF5F6800C48A62 /* mem.c in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -66,6 +66,8 @@ struct ContentView: View { @State var profile_open: Bool = false @State var thread_open: Bool = false @State var search_open: Bool = false + @State var wallet_open: Bool = false + @State var active_nwc: WalletConnectURL? = nil @State var muting: String? = nil @State var confirm_mute: Bool = false @State var user_muted_confirm: Bool = false @@ -131,6 +133,7 @@ struct ContentView: View { profile_open = false thread_open = false search_open = false + wallet_open = false isSideBarOpened = false } @@ -141,6 +144,9 @@ struct ContentView: View { func MainContent(damus: DamusState) -> some View { VStack { + NavigationLink(destination: WalletView(model: damus_state!.wallet), isActive: $wallet_open) { + EmptyView() + } NavigationLink(destination: MaybeProfileView, isActive: $profile_open) { EmptyView() } @@ -235,6 +241,11 @@ struct ContentView: View { self.thread_open = true } + func open_wallet(nwc: WalletConnectURL) { + self.damus_state!.wallet.new(nwc) + self.wallet_open = true + } + func open_profile(id: String) { self.active_profile = id self.profile_open = true @@ -320,29 +331,17 @@ struct ContentView: View { } } .onOpenURL { url in - guard let link = decode_nostr_uri(url.absoluteString) else { - return - } - - switch link { - case .ref(let ref): - if ref.key == "p" { - active_profile = ref.ref_id - profile_open = true - } else if ref.key == "e" { - find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in - if let ev { - open_event(ev: ev) - } - } + on_open_url(state: damus_state!, url: url) { res in + guard let res else { + return } - case .filter(let filt): - active_search = filt - search_open = true - break - // TODO: handle filter searches? + + switch res { + case .filter(let filt): self.open_search(filt: filt) + case .profile(let id): self.open_profile(id: id) + case .event(let ev): self.open_event(ev: ev) + case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)} } - } .onReceive(handle_notify(.compose)) { notif in let action = notif.object as! PostAction @@ -589,7 +588,8 @@ struct ContentView: View { postbox: PostBox(pool: pool), bootstrap_relays: bootstrap_relays, replies: ReplyCounter(our_pubkey: pubkey), - muted_threads: MutedThreadsManager(keypair: keypair) + muted_threads: MutedThreadsManager(keypair: keypair), + wallet: WalletModel(settings: settings) ) home.damus_state = self.damus_state! @@ -839,3 +839,40 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev return false } } + + +enum OpenResult { + case profile(String) + case filter(NostrFilter) + case event(NostrEvent) + case wallet_connect(WalletConnectURL) +} + +func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> Void) { + if let nwc = WalletConnectURL(str: url.absoluteString) { + result(.wallet_connect(nwc)) + return + } + + guard let link = decode_nostr_uri(url.absoluteString) else { + result(nil) + return + } + + switch link { + case .ref(let ref): + if ref.key == "p" { + result(.profile(ref.ref_id)) + } else if ref.key == "e" { + find_event(state: state, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in + if let ev { + result(.event(ev)) + } + } + } + case .filter(let filt): + result(.filter(filt)) + break + // TODO: handle filter searches? + } +} diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift @@ -29,6 +29,7 @@ struct DamusState { let bootstrap_relays: [String] let replies: ReplyCounter let muted_threads: MutedThreadsManager + let wallet: WalletModel @discardableResult func add_zap(zap: Zap) -> Bool { @@ -47,5 +48,5 @@ 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: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil))) } + return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel()) } } diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift @@ -48,11 +48,17 @@ struct SideMenuView: View { navLabel(title: NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), systemImage: "person") } - /* - NavigationLink(destination: EmptyView()) { - navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), systemImage: "bolt") + NavigationLink(destination: WalletView(model: damus_state.wallet)) { + HStack { + Image("wallet") + .tint(DamusColors.adaptableBlack) + + Text(NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view.")) + .font(.title2) + .foregroundColor(textColor()) + .frame(maxWidth: .infinity, alignment: .leading) + } } - */ NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) { navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), systemImage: "exclamationmark.octagon") diff --git a/damus/Views/Wallet/ConnectWalletView.swift b/damus/Views/Wallet/ConnectWalletView.swift @@ -0,0 +1,98 @@ +// +// ConnectWalletView.swift +// damus +// +// Created by William Casarin on 2023-05-05. +// + +import SwiftUI + +struct ConnectWalletView: View { + @Environment(\.openURL) private var openURL + @ObservedObject var model: WalletModel + + @State var scanning: Bool = false + @State var error: String? = nil + @State var wallet_scan_result: WalletScanResult = .scanning + + var body: some View { + MainContent + .navigationTitle("Attach a Wallet") + .navigationBarTitleDisplayMode(.large) + .padding() + .onChange(of: wallet_scan_result) { res in + scanning = false + + switch res { + case .success(let url): + error = nil + self.model.new(url) + + case .failed: + error = "Invalid nostr wallet connection string" + + case .scanning: + error = nil + } + } + } + + func AreYouSure(nwc: WalletConnectURL) -> some View { + VStack { + Text("Are you sure you want to attach this wallet?") + .font(.title) + + Text(nwc.relay.id) + .font(.body) + .foregroundColor(.gray) + + BigButton("Attach") { + model.connect(nwc) + } + + BigButton("Cancel") { + model.cancel() + } + } + } + + var ConnectWallet: some View { + VStack { + NavigationLink(destination: WalletScannerView(result: $wallet_scan_result), isActive: $scanning) { + EmptyView() + } + + AlbyButton() { + openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!) + } + + BigButton("Attach Wallet") { + scanning = true + } + + if let err = self.error { + Text(err) + .foregroundColor(.red) + } + } + } + + var MainContent: some View { + Group { + switch model.connect_state { + case .new(let nwc): + AreYouSure(nwc: nwc) + case .existing: + Text("Shouldn't happen") + case .none: + ConnectWallet + } + } + } +} + +struct ConnectWalletView_Previews: PreviewProvider { + static var previews: some View { + ConnectWalletView(model: WalletModel()) + } +} diff --git a/damus/Views/Wallet/NWCScannerView.swift b/damus/Views/Wallet/NWCScannerView.swift @@ -0,0 +1,77 @@ +// +// QRScannerView.swift +// damus +// +// Created by William Casarin on 2023-05-09. +// + +import SwiftUI + +enum WalletScanResult: Equatable { + static func == (lhs: WalletScanResult, rhs: WalletScanResult) -> Bool { + switch lhs { + case .success(let a): + switch rhs { + case .success(let b): + return a == b + case .failed: + return false + case .scanning: + return false + } + case .failed: + switch rhs { + case .success: + return false + case .failed: + return true + case .scanning: + return false + } + case .scanning: + switch rhs { + case .success: + return false + case .failed: + return false + case .scanning: + return true + } + } + } + + case success(WalletConnectURL) + case failed + case scanning +} + +struct WalletScannerView: View { + @Binding var result: WalletScanResult + + @Environment(\.dismiss) var dismiss + + var body: some View { + CodeScannerView(codeTypes: [.qr]) { res in + switch res { + case .success(let success): + guard let url = WalletConnectURL(str: success.string) else { + result = .failed + return + } + + result = .success(url) + case .failure: + result = .failed + } + + dismiss() + } + } +} + +struct QRScannerView_Previews: PreviewProvider { + @State static var result: WalletScanResult = .scanning + static var previews: some View { + WalletScannerView(result: $result) + } +} diff --git a/damus/Views/Wallet/WalletView.swift b/damus/Views/Wallet/WalletView.swift @@ -0,0 +1,40 @@ +// +// WalletView.swift +// damus +// +// Created by William Casarin on 2023-05-05. +// + +import SwiftUI + +struct WalletView: View { + @ObservedObject var model: WalletModel + + func MainWalletView(nwc: WalletConnectURL) -> some View { + VStack { + Text("\(nwc.relay.id)") + + BigButton("Disconnect Wallet") { + self.model.disconnect() + } + } + .padding() + } + + var body: some View { + switch model.connect_state { + case .new: + ConnectWalletView(model: model) + case .none: + ConnectWalletView(model: model) + case .existing(let nwc): + MainWalletView(nwc: nwc) + } + } +} + +struct WalletView_Previews: PreviewProvider { + static var previews: some View { + WalletView(model: WalletModel()) + } +}