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 }