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 }