damus

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

ProfileActionSheetView.swift (13925B)


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