damus

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

QRCodeView.swift (10252B)


      1 //
      2 //  QRCodeView.swift
      3 //  damus
      4 //
      5 //  Created by eric on 1/27/23.
      6 //
      7 
      8 import SwiftUI
      9 import CoreImage.CIFilterBuiltins
     10 
     11 struct ProfileScanResult: Equatable {
     12     let pubkey: Pubkey
     13 
     14     init?(hex: String) {
     15         guard let pk = hex_decode(hex).map({ bytes in Pubkey(Data(bytes)) }) else {
     16             return nil
     17         }
     18 
     19         self.pubkey = pk
     20     }
     21     
     22     init?(string: String) {
     23         var str = string
     24         guard str.count != 0 else {
     25             return nil
     26         }
     27         
     28         if str.hasPrefix("nostr:") {
     29             str.removeFirst("nostr:".count)
     30         }
     31         
     32         if let decoded = hex_decode(str),
     33            str.count == 64
     34         {
     35             self.pubkey = Pubkey(Data(decoded))
     36             return
     37         }
     38         
     39         if str.starts(with: "npub"),
     40            let b32 = try? bech32_decode(str)
     41         {
     42             self.pubkey = Pubkey(b32.data)
     43             return
     44         }
     45         
     46         return nil
     47     }
     48 }
     49 
     50 struct QRCodeView: View {
     51     let damus_state: DamusState
     52     @State var pubkey: Pubkey
     53     
     54     @Environment(\.presentationMode) var presentationMode
     55     
     56     @State private var selectedTab = 0
     57     @State var scanResult: ProfileScanResult? = nil
     58     @State var profile: Profile? = nil
     59     @State var error: String? = nil
     60     @State private var outerTrimEnd: CGFloat = 0
     61 
     62     var animationDuration: Double = 0.5
     63     
     64     let generator = UIImpactFeedbackGenerator(style: .light)
     65 
     66     @ViewBuilder
     67     func navImage(systemImage: String) -> some View {
     68         Image(systemName: systemImage)
     69             .frame(width: 33, height: 33)
     70             .background(Color.black.opacity(0.6))
     71             .clipShape(Circle())
     72     }
     73     
     74     var navBackButton: some View {
     75         Button {
     76             presentationMode.wrappedValue.dismiss()
     77         } label: {
     78             navImage(systemImage: "chevron.left")
     79         }
     80     }
     81     
     82     var customNavbar: some View {
     83         HStack {
     84             navBackButton
     85             Spacer()
     86         }
     87         .padding(.top, 5)
     88         .padding(.horizontal)
     89         .accentColor(DamusColors.white)
     90     }
     91     
     92     var body: some View {
     93         NavigationView {
     94             ZStack(alignment: .center) {
     95                 ZStack(alignment: .topLeading) {
     96                     DamusGradient()
     97                 }
     98                 TabView(selection: $selectedTab) {
     99                     QRView
    100                         .tag(0)
    101                     QRCameraView()
    102                         .tag(1)
    103                 }
    104                 .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    105                 .onAppear {
    106                     UIScrollView.appearance().isScrollEnabled = false
    107                 }
    108                 .gesture(
    109                     DragGesture()
    110                         .onChanged { _ in }
    111                 )
    112             }
    113         }
    114         .navigationTitle("")
    115         .navigationBarHidden(true)
    116         .overlay(customNavbar, alignment: .top)
    117     }
    118     
    119     var QRView: some View {
    120         VStack(alignment: .center) {
    121             let profile_txn = damus_state.profiles.lookup(id: pubkey, txn_name: "qrview-profile")
    122             let profile = profile_txn?.unsafeUnownedValue
    123             let our_profile = profile_txn.flatMap({ ptxn in
    124                 damus_state.ndb.lookup_profile_with_txn(damus_state.pubkey, txn: ptxn)?.profile
    125             })
    126 
    127             if our_profile?.picture != nil {
    128                 ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
    129                     .padding(.top, 50)
    130             } else {
    131                 Image(systemName: "person.fill")
    132                     .font(.system(size: 60))
    133                     .padding(.top, 50)
    134             }
    135             
    136             if let display_name = profile?.display_name {
    137                 Text(display_name)
    138                     .font(.system(size: 24, weight: .heavy))
    139                     .foregroundColor(.white)
    140             }
    141             if let name = profile?.name {
    142                 Text("@" + name)
    143                     .font(.body)
    144                     .foregroundColor(.white)
    145             }
    146             
    147             Spacer()
    148             
    149             Image(uiImage: generateQRCode(pubkey: "nostr:" + pubkey.npub))
    150                 .interpolation(.none)
    151                 .resizable()
    152                 .scaledToFit()
    153                 .frame(width: 300, height: 300)
    154                 .cornerRadius(10)
    155                 .overlay(RoundedRectangle(cornerRadius: 10)
    156                     .stroke(DamusColors.white, lineWidth: 5.0))
    157                 .shadow(radius: 10)
    158 
    159             Spacer()
    160             
    161             Text("Follow me on Nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
    162                 .font(.system(size: 24, weight: .heavy))
    163                 .padding(.top)
    164                 .foregroundColor(.white)
    165             
    166             Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
    167                 .font(.system(size: 18, weight: .ultraLight))
    168                 .foregroundColor(.white)
    169             
    170             Spacer()
    171             
    172             Button(action: {
    173                 selectedTab = 1
    174             }) {
    175                 HStack {
    176                     Text("Scan Code", comment: "Button to switch to scan QR Code page.")
    177                         .fontWeight(.semibold)
    178                 }
    179                 .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
    180             }
    181             .buttonStyle(GradientButtonStyle())
    182             .padding(50)
    183         }
    184     }
    185     
    186     func QRCameraView() -> some View {
    187         return VStack(alignment: .center) {
    188             Text("Scan a user's pubkey", comment: "Text to prompt scanning a QR code of a user's pubkey to open their profile.")
    189                 .padding(.top, 50)
    190                 .font(.system(size: 24, weight: .heavy))
    191                 .foregroundColor(.white)
    192             
    193             Spacer()
    194 
    195             CodeScannerView(codeTypes: [.qr], scanMode: .continuous, simulatedData: "npub1k92qsr95jcumkpu6dffurkvwwycwa2euvx4fthv78ru7gqqz0nrs2ngfwd", shouldVibrateOnSuccess: false) { result in
    196                 switch result {
    197                 case .success(let success):
    198                     handleProfileScan(success.string)
    199                 case .failure(let failure):
    200                     self.error = failure.localizedDescription
    201                 }
    202             }
    203             .scaledToFit()
    204             .frame(width: 300, height: 300)
    205             .cornerRadius(10)
    206             .overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0))
    207             .overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5)
    208             .rotationEffect(.degrees(-90)))
    209             .shadow(radius: 10)
    210             
    211             Spacer()
    212             
    213             Spacer()
    214             
    215             Button(action: {
    216                 selectedTab = 0
    217             }) {
    218                 HStack {
    219                     Text("View QR Code", comment: "Button to switch to view users QR Code")
    220                         .fontWeight(.semibold)
    221                 }
    222                 .frame( maxWidth: .infinity, maxHeight: 12, alignment: .center)
    223             }
    224             .buttonStyle(GradientButtonStyle())
    225             .padding(50)
    226         }
    227     }
    228     
    229     func handleProfileScan(_ scanned_str: String) {
    230         guard let result = ProfileScanResult(string: scanned_str) else {
    231             self.error = "Invalid profile QR"
    232             return
    233         }
    234         
    235         self.error = nil
    236 
    237         guard result != self.scanResult else {
    238             return
    239         }
    240         
    241         generator.impactOccurred()
    242         cameraAnimate {
    243             scanResult = result
    244             
    245             find_event(state: damus_state, query: .profile(pubkey: result.pubkey)) { res in
    246                 guard let res else {
    247                     error = "Profile not found"
    248                     return
    249                 }
    250                 
    251                 switch res {
    252                 case .invalid_profile:
    253                     error = "Profile was found but was corrupt."
    254                     
    255                 case .profile:
    256                     show_profile_after_delay()
    257                     
    258                 case .event:
    259                     print("invalid search result")
    260                 }
    261                 
    262             }
    263         }
    264     }
    265     
    266     func show_profile_after_delay() {
    267         DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {
    268             if let scanResult {
    269                 damus_state.nav.push(route: Route.ProfileByKey(pubkey: scanResult.pubkey))
    270                 presentationMode.wrappedValue.dismiss()
    271             }
    272         }
    273     }
    274 
    275     func cameraAnimate(completion: @escaping () -> Void) {
    276         outerTrimEnd = 0.0
    277         withAnimation(.easeInOut(duration: animationDuration)) {
    278             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.
    279         }
    280         completion()
    281     }
    282     
    283     func generateQRCode(pubkey: String) -> UIImage {
    284         let data = pubkey.data(using: String.Encoding.ascii)
    285         let qrFilter = CIFilter(name: "CIQRCodeGenerator")
    286         qrFilter?.setValue(data, forKey: "inputMessage")
    287         let qrImage = qrFilter?.outputImage
    288         
    289         let colorInvertFilter = CIFilter(name: "CIColorInvert")
    290         colorInvertFilter?.setValue(qrImage, forKey: "inputImage")
    291         let outputInvertedImage = colorInvertFilter?.outputImage
    292         
    293         let maskToAlphaFilter = CIFilter(name: "CIMaskToAlpha")
    294         maskToAlphaFilter?.setValue(outputInvertedImage, forKey: "inputImage")
    295         let outputCIImage = maskToAlphaFilter?.outputImage
    296 
    297         let context = CIContext()
    298         let cgImage = context.createCGImage(outputCIImage!, from: outputCIImage!.extent)!
    299         return UIImage(cgImage: cgImage)
    300     }
    301 }
    302 
    303 struct QRCodeView_Previews: PreviewProvider {
    304     static var previews: some View {
    305         QRCodeView(damus_state: test_damus_state, pubkey: test_note.pubkey)
    306     }
    307 }
    308