damus

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

ProfileActionSheetView.swift (12965B)


      1 //
      2 //  ProfileActionSheetView.swift
      3 //  damus
      4 //
      5 //  Created by Daniel D’Aquino on 2023-10-20.
      6 //
      7 
      8 import SwiftUI
      9 
     10 struct ProfileActionSheetView: View {
     11     let damus_state: DamusState
     12     let pfp_size: CGFloat = 90.0
     13 
     14     @StateObject var profile: ProfileModel
     15     @StateObject var zap_button_model: ZapButtonModel = ZapButtonModel()
     16     @State private var sheetHeight: CGFloat = .zero
     17     
     18     @Environment(\.dismiss) var dismiss
     19     @Environment(\.colorScheme) var colorScheme
     20     @Environment(\.presentationMode) var presentationMode
     21     
     22     init(damus_state: DamusState, pubkey: Pubkey) {
     23         self.damus_state = damus_state
     24         self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state))
     25     }
     26 
     27     func imageBorderColor() -> Color {
     28         colorScheme == .light ? DamusColors.white : DamusColors.black
     29     }
     30     
     31     func profile_data() -> ProfileRecord? {
     32         let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey)
     33         return profile_txn?.unsafeUnownedValue
     34     }
     35     
     36     func get_profile() -> Profile? {
     37         return self.profile_data()?.profile
     38     }
     39     
     40     var followButton: some View {
     41         return ProfileActionSheetFollowButton(
     42             target: .pubkey(self.profile.pubkey),
     43             follows_you: self.profile.follows(pubkey: damus_state.pubkey),
     44             follow_state: damus_state.contacts.follow_state(profile.pubkey)
     45         )
     46     }
     47     
     48     var dmButton: some View {
     49         let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
     50         return VStack(alignment: .center, spacing: 10) {
     51             Button(
     52                 action: {
     53                     damus_state.nav.push(route: Route.DMChat(dms: dm_model))
     54                     dismiss()
     55                 },
     56                 label: {
     57                     Image("messages")
     58                         .profile_button_style(scheme: colorScheme)
     59                 }
     60             )
     61             .buttonStyle(NeutralButtonShape.circle.style)
     62             Text("Message", comment: "Button label that allows the user to start a direct message conversation with the user shown on-screen")
     63                 .foregroundStyle(.secondary)
     64                 .font(.caption)
     65         }
     66     }
     67     
     68     var zapButton: some View {
     69         if let lnurl = self.profile_data()?.lnurl, lnurl != "" {
     70             return AnyView(ProfileActionSheetZapButton(damus_state: damus_state, profile: profile, lnurl: lnurl))
     71         }
     72         else {
     73             return AnyView(EmptyView())
     74         }
     75     }
     76     
     77     var profileName: some View {
     78         let display_name = Profile.displayName(profile: self.get_profile(), pubkey: self.profile.pubkey).displayName
     79         return HStack(alignment: .center, spacing: 10) {
     80             Text(display_name)
     81                 .font(.title)
     82         }
     83     }
     84     
     85     var body: some View {
     86         VStack(alignment: .center) {
     87             ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
     88             if let url = self.profile_data()?.profile?.website_url {
     89                 WebsiteLink(url: url, style: .accent)
     90                     .padding(.top, -15)
     91             }
     92             
     93             profileName
     94             
     95             PubkeyView(pubkey: profile.pubkey)
     96             
     97             if let about = self.profile_data()?.profile?.about {
     98                 AboutView(state: damus_state, about: about, max_about_length: 140, text_alignment: .center)
     99                     .padding(.top)
    100             }
    101             
    102             HStack(spacing: 20) {
    103                 self.followButton
    104                 self.zapButton
    105                 self.dmButton
    106             }
    107             .padding()
    108             
    109             Button(
    110                 action: {
    111                     damus_state.nav.push(route: Route.ProfileByKey(pubkey: profile.pubkey))
    112                     dismiss()
    113                 },
    114                 label: {
    115                     HStack {
    116                         Spacer()
    117                         Text("View full profile", comment: "A button label that allows the user to see the full profile of the profile they are previewing")
    118                         Image(systemName: "arrow.up.right")
    119                         Spacer()
    120                     }
    121                     
    122                 }
    123             )
    124             .buttonStyle(NeutralButtonShape.circle.style)
    125         }
    126         .padding()
    127         .padding(.top, 20)
    128         .overlay {
    129             GeometryReader { geometry in
    130                 Color.clear.preference(key: InnerHeightPreferenceKey.self, value: geometry.size.height)
    131             }
    132         }
    133         .onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in
    134             sheetHeight = newHeight
    135         }
    136         .presentationDetents([.height(sheetHeight)])
    137     }
    138 }
    139 
    140 fileprivate struct ProfileActionSheetFollowButton: View {
    141     @Environment(\.colorScheme) var colorScheme
    142     
    143     let target: FollowTarget
    144     let follows_you: Bool
    145     @State var follow_state: FollowState
    146     
    147     var body: some View {
    148         VStack(alignment: .center, spacing: 10) {
    149             Button(
    150                 action: {
    151                     follow_state = perform_follow_btn_action(follow_state, target: target)
    152                 },
    153                 label: {
    154                     switch follow_state {
    155                         case .unfollows:
    156                             Image("user-add-down")
    157                                 .foregroundColor(Color.primary)
    158                                 .profile_button_style(scheme: colorScheme)
    159                         default:
    160                             Image("user-added")
    161                                 .foregroundColor(Color.green)
    162                                 .profile_button_style(scheme: colorScheme)
    163                     }
    164                     
    165                 }
    166             )
    167             .buttonStyle(NeutralButtonShape.circle.style)
    168             
    169             Text(verbatim: "\(follow_btn_txt(follow_state, follows_you: follows_you))")
    170             .foregroundStyle(.secondary)
    171             .font(.caption)
    172         }
    173         .onReceive(handle_notify(.followed)) { follow in
    174             guard case .pubkey(let pk) = follow,
    175                   pk == target.pubkey else { return }
    176 
    177             self.follow_state = .follows
    178         }
    179         .onReceive(handle_notify(.unfollowed)) { unfollow in
    180             guard case .pubkey(let pk) = unfollow,
    181                   pk == target.pubkey else { return }
    182 
    183             self.follow_state = .unfollows
    184         }
    185     }
    186 }
    187     
    188 
    189 fileprivate struct ProfileActionSheetZapButton: View {
    190     enum ZappingState: Equatable {
    191         case not_zapped
    192         case zapping
    193         case zap_success
    194         case zap_failure(error: ZappingError)
    195         
    196         func error_message() -> String? {
    197             switch self {
    198                 case .zap_failure(let error):
    199                     return error.humanReadableMessage()
    200                 default:
    201                     return nil
    202             }
    203         }
    204     }
    205     
    206     let damus_state: DamusState
    207     @StateObject var profile: ProfileModel
    208     let lnurl: String
    209     @State var zap_state: ZappingState = .not_zapped
    210     @State var show_error_alert: Bool = false
    211     
    212     @Environment(\.colorScheme) var colorScheme
    213     
    214     func receive_zap(zap_ev: ZappingEvent) {
    215         print("Received zap event")
    216         guard zap_ev.target == ZapTarget.profile(self.profile.pubkey) else {
    217             return
    218         }
    219         
    220         switch zap_ev.type {
    221             case .failed(let err):
    222                 zap_state = .zap_failure(error: err)
    223                 show_error_alert = true
    224                 break
    225             case .got_zap_invoice(let inv):
    226                 if damus_state.settings.show_wallet_selector {
    227                     present_sheet(.select_wallet(invoice: inv))
    228                 } else {
    229                     let wallet = damus_state.settings.default_wallet.model
    230                     do {
    231                         try open_with_wallet(wallet: wallet, invoice: inv)
    232                     }
    233                     catch {
    234                         present_sheet(.select_wallet(invoice: inv))
    235                     }
    236                 }
    237                 break
    238             case .sent_from_nwc:
    239                 zap_state = .zap_success
    240                 break
    241         }
    242     }
    243     
    244     var button_label: String {
    245         switch zap_state {
    246             case .not_zapped:
    247                 return NSLocalizedString("Zap", comment: "Button label that allows the user to zap (i.e. send a Bitcoin tip via the lightning network) the user shown on-screen")
    248             case .zapping:
    249                 return NSLocalizedString("Zapping", comment: "Button label indicating that a zap action is in progress (i.e. the user is currently sending a Bitcoin tip via the lightning network to the user shown on-screen) ")
    250             case .zap_success:
    251                 return NSLocalizedString("Zapped!", comment: "Button label indicating that a zap action was successful (i.e. the user is successfully sent a Bitcoin tip via the lightning network to the user shown on-screen) ")
    252             case .zap_failure(_):
    253                 return NSLocalizedString("Zap failed", comment: "Button label indicating that a zap action was unsuccessful (i.e. the user was unable to send a Bitcoin tip via the lightning network to the user shown on-screen) ")
    254         }
    255     }
    256     
    257     var body: some View {
    258         VStack(alignment: .center, spacing: 10) {
    259             Button(
    260                 action: {
    261                     send_zap(damus_state: damus_state, target: .profile(self.profile.pubkey), lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
    262                     zap_state = .zapping
    263                 },
    264                 label: {
    265                     switch zap_state {
    266                         case .not_zapped:
    267                             Image("zap")
    268                                 .foregroundColor(Color.primary)
    269                                 .profile_button_style(scheme: colorScheme)
    270                         case .zapping:
    271                             ProgressView()
    272                                 .foregroundColor(Color.primary)
    273                                 .profile_button_style(scheme: colorScheme)
    274                         case .zap_success:
    275                             Image("checkmark")
    276                                 .foregroundColor(Color.green)
    277                                 .profile_button_style(scheme: colorScheme)
    278                         case .zap_failure:
    279                             Image("close")
    280                                 .foregroundColor(Color.red)
    281                                 .profile_button_style(scheme: colorScheme)
    282                     }
    283                     
    284                 }
    285             )
    286             .disabled({
    287                 switch zap_state {
    288                     case .not_zapped:
    289                         return false
    290                     default:
    291                         return true
    292                 }
    293             }())
    294             .buttonStyle(NeutralButtonShape.circle.style)
    295             
    296             Text(button_label)
    297             .foregroundStyle(.secondary)
    298             .font(.caption)
    299         }
    300         .onReceive(handle_notify(.zapping)) { zap_ev in
    301             receive_zap(zap_ev: zap_ev)
    302         }
    303         .simultaneousGesture(LongPressGesture().onEnded {_  in
    304             present_sheet(.zap(target: .profile(self.profile.pubkey), lnurl: lnurl))
    305         })
    306         .alert(isPresented: $show_error_alert) {
    307             Alert(
    308                 title: Text("Zap failed", comment: "Title of an alert indicating that a zap action failed"),
    309                 message: Text(zap_state.error_message() ?? ""),
    310                 dismissButton: .default(Text("OK", comment: "Button label to dismiss an error dialog"))
    311             )
    312         }
    313         .onChange(of: zap_state) { new_zap_state in
    314             switch new_zap_state {
    315                 case .zap_success, .zap_failure:
    316                     DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    317                         withAnimation {
    318                             zap_state = .not_zapped
    319                         }
    320                     }
    321                     break
    322                 default:
    323                     break
    324             }
    325         }
    326     }
    327 }
    328 
    329 struct InnerHeightPreferenceKey: PreferenceKey {
    330     static var defaultValue: CGFloat = .zero
    331     static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
    332         value = nextValue()
    333     }
    334 }
    335 
    336 func show_profile_action_sheet_if_enabled(damus_state: DamusState, pubkey: Pubkey) {
    337     if damus_state.settings.show_profile_action_sheet_on_pfp_click {
    338         notify(.present_sheet(Sheets.profile_action(pubkey)))
    339     }
    340     else {
    341         damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
    342     }
    343 }
    344 
    345 #Preview {
    346     ProfileActionSheetView(damus_state: test_damus_state, pubkey: test_pubkey)
    347 }