damus

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

commit 5f2c8223bd15249ab425ec19bfefd4aa122befeb
parent 14977fe3dd679f0c2b68b515e8fd59cdc9269241
Author: Suhail Saqan <suhail.saqan@gmail.com>
Date:   Thu,  1 Jun 2023 14:49:56 -0500

Add qr code scanner

Changelog-Added: Add qr code scanner
Closes: #733

Diffstat:
Mdamus/Views/QRCodeView.swift | 298+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
1 file changed, 230 insertions(+), 68 deletions(-)

diff --git a/damus/Views/QRCodeView.swift b/damus/Views/QRCodeView.swift @@ -13,6 +13,20 @@ struct QRCodeView: View { @State var pubkey: String @Environment(\.presentationMode) var presentationMode + + @State private var selectedTab = 0 + + @State var scanResult: Search? = nil + + @State var showProfileView: Bool = false + @State var profile: Profile? = nil + + @State private var scannedCode = "" + + @State private var outerTrimEnd: CGFloat = 0 + var animationDuration: Double = 0.5 + + let generator = UIImpactFeedbackGenerator(style: .light) var maybe_key: String? { guard let key = bech32_pubkey(pubkey) else { @@ -22,87 +36,235 @@ struct QRCodeView: View { return key } + @ViewBuilder + func navImage(systemImage: String) -> some View { + Image(systemName: systemImage) + .frame(width: 33, height: 33) + .background(Color.black.opacity(0.6)) + .clipShape(Circle()) + } + + var navBackButton: some View { + Button { + presentationMode.wrappedValue.dismiss() + } label: { + navImage(systemImage: "chevron.left") + } + } + + var customNavbar: some View { + HStack { + navBackButton + Spacer() + } + .padding(.top, 5) + .padding(.horizontal) + .accentColor(DamusColors.white) + } + var body: some View { - ZStack(alignment: .center) { + NavigationView { + ZStack(alignment: .center) { + ZStack(alignment: .topLeading) { + DamusGradient() + } + TabView(selection: $selectedTab) { + QRView + .tag(0) + if pubkey == damus_state.pubkey { + QRCameraView() + .tag(1) + } + } + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) + .onAppear { + UIScrollView.appearance().isScrollEnabled = false + } + .gesture( + DragGesture() + .onChanged { _ in } + ) + } + } + .navigationTitle("") + .navigationBarHidden(true) + .overlay(customNavbar, alignment: .top) + } + + var QRView: some View { + VStack(alignment: .center) { + let profile = damus_state.profiles.lookup(id: pubkey) + + if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil { + ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) + .padding(.top, 50) + } else { + Image(systemName: "person.fill") + .font(.system(size: 60)) + .padding(.top, 50) + } - ZStack(alignment: .topLeading) { - DamusGradient() - Button { - presentationMode.wrappedValue.dismiss() - } label: { - Image("close") - .foregroundColor(.white) - .font(.subheadline) - .padding(.leading, 20) + if let display_name = profile?.display_name { + Text(display_name) + .font(.system(size: 24, weight: .heavy)) + } + if let name = profile?.name { + Text("@" + name) + .font(.body) + } + + Spacer() + + if let key = maybe_key { + Image(uiImage: generateQRCode(pubkey: "nostr:" + key)) + .interpolation(.none) + .resizable() + .scaledToFit() + .frame(width: 300, height: 300) + .cornerRadius(10) + .overlay(RoundedRectangle(cornerRadius: 10) + .stroke(DamusColors.white, lineWidth: 5.0)) + .shadow(radius: 10) + } + + Spacer() + + Text("Follow me on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.") + .font(.system(size: 24, weight: .heavy)) + .padding(.top) + + Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.") + .font(.system(size: 18, weight: .ultraLight)) + + Spacer() + + Button(action: { + selectedTab = 1 + }) { + HStack { + Text("Scan Code", comment: "Button to switch to scan QR Code page.") + .fontWeight(.semibold) } - .zIndex(1) + .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) } + .buttonStyle(GradientButtonStyle()) + .padding(50) + } + } + + func search_changed(_ new: String) { + var str = new + guard str.count != 0 else { + return + } - VStack(alignment: .center) { - - let profile = damus_state.profiles.lookup(id: pubkey) - - if (damus_state.profiles.lookup(id: pubkey)?.picture) != nil { - ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) - .padding(.top, 50) - } else { - Image(systemName: "person.fill") - .font(.system(size: 60)) - .foregroundColor(DamusColors.white) - .padding(.top, 50) - } - - if let display_name = profile?.display_name { - Text(display_name) - .foregroundColor(DamusColors.white) - .font(.system(size: 24, weight: .heavy)) - } - if let name = profile?.name { - Text("@" + name) - .foregroundColor(DamusColors.white) - .font(.body) + if str.hasPrefix("nostr:") { + str.removeFirst("nostr:".count) + } + + if let _ = hex_decode(str), str.count == 64 { + self.scanResult = .hex(str) + return + } + + if str.starts(with: "npub") { + if let _ = try? bech32_decode(str) { + self.scanResult = .profile(str) + return + } + } + } + + func QRCameraView() -> some View { + return VStack(alignment: .center) { + Text("Scan a user's pubkey") + .padding(.top, 50) + .font(.system(size: 24, weight: .heavy)) + + Spacer() + + CodeScannerView(codeTypes: [.qr], scanMode: .continuous, simulatedData: "npub1k92qsr95jcumkpu6dffurkvwwycwa2euvx4fthv78ru7gqqz0nrs2ngfwd", shouldVibrateOnSuccess: false) { result in + switch result { + case .success(let result): + search_changed(result.string) + switch scanResult { + case .profile(let prof): + handleProfileScan(prof) + default: + print("Not a profile") + } + case .failure(let error): + print(error.localizedDescription) } + } + .scaledToFit() + .frame(width: 300, height: 300) + .cornerRadius(10) + .overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0)) + .overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5) + .rotationEffect(.degrees(-90))) + .shadow(radius: 10) + + Spacer() + + if showProfileView { + let decoded = try? bech32_decode(scannedCode) + let hex = hex_encode(decoded!.data) - Spacer() - - if let key = maybe_key { - Image(uiImage: generateQRCode(pubkey: "nostr:" + key)) - .interpolation(.none) - .resizable() - .scaledToFit() - .frame(width: 200, height: 200) - .padding() - .cornerRadius(10) - .overlay(RoundedRectangle(cornerRadius: 10) - .stroke(DamusColors.white, lineWidth: 1)) - .shadow(radius: 10) + NavigationLink( + destination: ProfileView(damus_state: damus_state, pubkey: hex), + isActive: $showProfileView, + label: { + EmptyView() + } + ) + } + + Spacer() + + Button(action: { + selectedTab = 0 + }) { + HStack { + Text("View QR Code", comment: "Button to switch to view users QR Code") + .fontWeight(.semibold) } + .frame( maxWidth: .infinity, maxHeight: 12, alignment: .center) + } + .buttonStyle(GradientButtonStyle()) + .padding(50) + } + } + + func profile(for code: String) -> Profile? { + let decoded = try? bech32_decode(code) + let hex = hex_encode(decoded!.data) + return damus_state.profiles.lookup(id: hex) + } + + func handleProfileScan(_ prof: String) { + if scannedCode != prof { + generator.impactOccurred() + cameraAnimate { + scannedCode = prof - Spacer() - - if (pubkey == damus_state.pubkey) { - Text("Follow me on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.") - .foregroundColor(DamusColors.white) - .font(.system(size: 24, weight: .heavy)) - .padding(.top) + if profile(for: scannedCode) != nil { + DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { + showProfileView = true + } } else { - Text("Follow them on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user (someone else).") - .foregroundColor(DamusColors.white) - .font(.system(size: 24, weight: .heavy)) - .padding(.top) + print("Profile not found") } - - Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.") - .foregroundColor(DamusColors.white) - .font(.system(size: 18, weight: .ultraLight)) - - Spacer() } - } - .modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: { - presentationMode.wrappedValue.dismiss() - })) + } + + func cameraAnimate(completion: @escaping () -> Void) { + outerTrimEnd = 0.0 + withAnimation(.easeInOut(duration: animationDuration)) { + outerTrimEnd = 1.05 // Set to 1.05 instead of 1.0 since sometimes `completion()` runs before the value reaches 1.0. This ensures the animation is done. + } + completion() } func generateQRCode(pubkey: String) -> UIImage {