diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index af675c89..5c988c4e 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -430,6 +430,7 @@ BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; }; + D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; }; D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; }; D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; }; @@ -437,6 +438,8 @@ D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; }; + D76874F32AE3632B00FB0F68 /* ZapButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */; }; + D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; }; D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; }; D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; }; D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; }; @@ -1136,6 +1139,8 @@ D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = ""; }; D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = ""; }; D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = ""; }; + D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButtonView.swift; sourceTree = ""; }; + D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = ""; }; D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = ""; }; D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = ""; }; D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = ""; }; @@ -1725,6 +1730,7 @@ 5C513FCB2984ACA60072348F /* QRCodeView.swift */, 643EA5C7296B764E005081BB /* RelayFilterView.swift */, D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */, + D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */, ); path = Views; sourceTree = ""; @@ -2236,6 +2242,7 @@ 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */, 4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */, 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */, + D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */, ); path = Zaps; sourceTree = ""; @@ -2960,6 +2967,8 @@ E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */, 4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */, 4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */, + D76874F32AE3632B00FB0F68 /* ZapButtonView.swift in Sources */, + D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */, 4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */, 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */, 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */, diff --git a/damus/Components/NeutralButtonStyle.swift b/damus/Components/NeutralButtonStyle.swift index 3d61b19e..f7aa797f 100644 --- a/damus/Components/NeutralButtonStyle.swift +++ b/damus/Components/NeutralButtonStyle.swift @@ -20,6 +20,20 @@ struct NeutralButtonStyle: ButtonStyle { } } +struct NeutralCircleButtonStyle: ButtonStyle { + func makeBody(configuration: Self.Configuration) -> some View { + return configuration.label + .padding(20) + .background(DamusColors.neutral1) + .cornerRadius(9999) + .overlay( + RoundedRectangle(cornerRadius: 9999) + .stroke(DamusColors.neutral3, lineWidth: 1) + ) + .scaleEffect(configuration.isPressed ? 0.95 : 1) + } +} + struct NeutralButtonStyle_Previews: PreviewProvider { static var previews: some View { diff --git a/damus/Components/SelectableText.swift b/damus/Components/SelectableText.swift index 51346f39..975d5810 100644 --- a/damus/Components/SelectableText.swift +++ b/damus/Components/SelectableText.swift @@ -11,12 +11,19 @@ import SwiftUI struct SelectableText: View { let attributedString: AttributedString + let textAlignment: NSTextAlignment @State private var selectedTextHeight: CGFloat = .zero @State private var selectedTextWidth: CGFloat = .zero let size: EventViewKind + init(attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) { + self.attributedString = attributedString + self.textAlignment = textAlignment ?? NSTextAlignment.natural + self.size = size + } + var body: some View { GeometryReader { geo in TextViewRepresentable( @@ -24,6 +31,7 @@ struct SelectableText: View { textColor: UIColor.label, font: eventviewsize_to_uifont(size), fixedWidth: selectedTextWidth, + textAlignment: self.textAlignment, height: $selectedTextHeight ) .padding([.leading, .trailing], -1.0) @@ -48,6 +56,7 @@ struct SelectableText: View { let textColor: UIColor let font: UIFont let fixedWidth: CGFloat + let textAlignment: NSTextAlignment @Binding var height: CGFloat @@ -61,12 +70,14 @@ struct SelectableText: View { view.textContainerInset = .zero view.textContainerInset.left = 1.0 view.textContainerInset.right = 1.0 + view.textAlignment = textAlignment return view } func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { let mutableAttributedString = createNSAttributedString() uiView.attributedText = mutableAttributedString + uiView.textAlignment = self.textAlignment let newHeight = mutableAttributedString.height(containerWidth: fixedWidth) diff --git a/damus/Components/WebsiteLink.swift b/damus/Components/WebsiteLink.swift index 90063dd0..8bb23f53 100644 --- a/damus/Components/WebsiteLink.swift +++ b/damus/Components/WebsiteLink.swift @@ -9,33 +9,57 @@ import SwiftUI struct WebsiteLink: View { let url: URL + let style: StyleVariant @Environment(\.openURL) var openURL + + init(url: URL, style: StyleVariant? = nil) { + self.url = url + self.style = style ?? .normal + } var body: some View { HStack { Image("link") - .foregroundColor(.gray) - .font(.footnote) + .resizable() + .frame(width: 16, height: 16) + .foregroundColor(self.style == .accent ? .white : .gray) + .padding(.vertical, 5) + .padding([.leading], 10) Button(action: { openURL(url) }, label: { Text(link_text) .font(.footnote) - .foregroundColor(.accentColor) + .foregroundColor(self.style == .accent ? .white : .accentColor) .truncationMode(.tail) .lineLimit(1) }) + .padding(.vertical, 5) + .padding([.trailing], 10) } + .background( + self.style == .accent ? + AnyView(RoundedRectangle(cornerRadius: 50).fill(PinkGradient)) + : AnyView(Color.clear) + ) } var link_text: String { url.host ?? url.absoluteString } + + enum StyleVariant { + case normal + case accent + } } struct WebsiteLink_Previews: PreviewProvider { static var previews: some View { WebsiteLink(url: URL(string: "https://jb55.com")!) + .previewDisplayName("Normal") + WebsiteLink(url: URL(string: "https://jb55.com")!, style: .accent) + .previewDisplayName("Accent") } } diff --git a/damus/ContentView.swift b/damus/ContentView.swift index edda8d50..2ab1cd68 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -22,6 +22,7 @@ enum Sheets: Identifiable { case post(PostAction) case report(ReportTarget) case event(NostrEvent) + case profile_action(Pubkey) case zap(ZapSheet) case select_wallet(SelectWallet) case filter @@ -42,6 +43,7 @@ enum Sheets: Identifiable { case .user_status: return "user_status" case .post(let action): return "post-" + (action.ev?.id.hex() ?? "") case .event(let ev): return "event-" + ev.id.hex() + case .profile_action(let pubkey): return "profile-action-" + pubkey.npub case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id) case .select_wallet: return "select-wallet" case .filter: return "filter" @@ -316,6 +318,8 @@ struct ContentView: View { .presentationDragIndicator(.visible) case .event: EventDetailView() + case .profile_action(let pubkey): + ProfileActionSheetView(damus_state: damus_state!, pubkey: pubkey) case .zap(let zapsheet): CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl) case .select_wallet(let select): diff --git a/damus/Views/Events/EventProfile.swift b/damus/Views/Events/EventProfile.swift index 6d807120..60525b13 100644 --- a/damus/Views/Events/EventProfile.swift +++ b/damus/Views/Events/EventProfile.swift @@ -37,9 +37,9 @@ struct EventProfile: View { var body: some View { HStack(alignment: .center, spacing: 10) { - ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation) + ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation, show_zappability: true) .onTapGesture { - damus_state.nav.push(route: .ProfileByKey(pubkey: pubkey)) + notify(.present_sheet(Sheets.profile_action(pubkey))) } VStack(alignment: .leading, spacing: 0) { diff --git a/damus/Views/Profile/AboutView.swift b/damus/Views/Profile/AboutView.swift index 948f86a2..905b8001 100644 --- a/damus/Views/Profile/AboutView.swift +++ b/damus/Views/Profile/AboutView.swift @@ -10,15 +10,23 @@ import SwiftUI struct AboutView: View { let state: DamusState let about: String - let max_about_length = 280 + let max_about_length: Int + let text_alignment: NSTextAlignment @State var show_full_about: Bool = false @State private var about_string: AttributedString? = nil + init(state: DamusState, about: String, max_about_length: Int? = nil, text_alignment: NSTextAlignment? = nil) { + self.state = state + self.about = about + self.max_about_length = max_about_length ?? 280 + self.text_alignment = text_alignment ?? .natural + } + var body: some View { Group { if let about_string { let truncated_about = show_full_about ? about_string : about_string.truncateOrNil(maxLength: max_about_length) - SelectableText(attributedString: truncated_about ?? about_string, size: .subheadline) + SelectableText(attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline) if truncated_about != nil { if show_full_about { diff --git a/damus/Views/Profile/MaybeAnonPfpView.swift b/damus/Views/Profile/MaybeAnonPfpView.swift index 0b4256e4..62c12393 100644 --- a/damus/Views/Profile/MaybeAnonPfpView.swift +++ b/damus/Views/Profile/MaybeAnonPfpView.swift @@ -21,16 +21,16 @@ struct MaybeAnonPfpView: View { } var body: some View { - Group { + ZStack { if is_anon { Image("question") .resizable() .font(.largeTitle) .frame(width: size, height: size) } else { - ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation) + ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true) .onTapGesture { - state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) + notify(.present_sheet(Sheets.profile_action(pubkey))) } } } diff --git a/damus/Views/Profile/ProfileNameView.swift b/damus/Views/Profile/ProfileNameView.swift index eb1821d7..44c585b4 100644 --- a/damus/Views/Profile/ProfileNameView.swift +++ b/damus/Views/Profile/ProfileNameView.swift @@ -7,84 +7,6 @@ import SwiftUI -fileprivate struct KeyView: View { - let pubkey: Pubkey - - @Environment(\.colorScheme) var colorScheme - - @State private var isCopied = false - - func keyColor() -> Color { - colorScheme == .light ? DamusColors.black : DamusColors.white - } - - private func copyPubkey(_ pubkey: String) { - UIPasteboard.general.string = pubkey - UIImpactFeedbackGenerator(style: .medium).impactOccurred() - withAnimation { - isCopied = true - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - withAnimation { - isCopied = false - } - } - } - } - - func pubkey_context_menu(pubkey: Pubkey) -> some View { - return self.contextMenu { - Button { - UIPasteboard.general.string = pubkey.npub - } label: { - Label(NSLocalizedString("Copy Account ID", comment: "Context menu option for copying the ID of the account that created the note."), image: "copy2") - } - } - } - - var body: some View { - let bech32 = pubkey.npub - - HStack { - Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))") - .font(.footnote) - .foregroundColor(keyColor()) - .padding(5) - .padding([.leading, .trailing], 5) - .background(RoundedRectangle(cornerRadius: 11).foregroundColor(DamusColors.adaptableGrey)) - - if isCopied { - HStack { - Image("check-circle") - .resizable() - .frame(width: 20, height: 20) - Text(NSLocalizedString("Copied", comment: "Label indicating that a user's key was copied.")) - .font(.footnote) - .layoutPriority(1) - } - .foregroundColor(DamusColors.green) - } else { - HStack { - Button { - copyPubkey(bech32) - } label: { - Label { - Text("Public key", comment: "Label indicating that the text is a user's public account key.") - } icon: { - Image("copy2") - .resizable() - .contentShape(Rectangle()) - .foregroundColor(.accentColor) - .frame(width: 20, height: 20) - } - .labelStyle(IconOnlyLabelStyle()) - .symbolRenderingMode(.hierarchical) - } - } - } - } - } -} - struct ProfileNameView: View { let pubkey: Pubkey let damus: DamusState @@ -116,7 +38,7 @@ struct ProfileNameView: View { Spacer() - KeyView(pubkey: pubkey) + PubkeyView(pubkey: pubkey) .pubkey_context_menu(pubkey: pubkey) } } diff --git a/damus/Views/Profile/ProfilePicView.swift b/damus/Views/Profile/ProfilePicView.swift index 54afbbc5..779b7e84 100644 --- a/damus/Views/Profile/ProfilePicView.swift +++ b/damus/Views/Profile/ProfilePicView.swift @@ -69,38 +69,59 @@ struct ProfilePicView: View { let highlight: Highlight let profiles: Profiles let disable_animation: Bool + let zappability_indicator: Bool @State var picture: String? - init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil) { + init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil, show_zappability: Bool? = nil) { self.pubkey = pubkey self.profiles = profiles self.size = size self.highlight = highlight self._picture = State(initialValue: picture) self.disable_animation = disable_animation + self.zappability_indicator = show_zappability ?? false + } + + func get_lnurl() -> String? { + return profiles.lookup_with_timestamp(pubkey).unsafeUnownedValue?.lnurl } var body: some View { - InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight, disable_animation: disable_animation) - .onReceive(handle_notify(.profile_updated)) { updated in - guard updated.pubkey == self.pubkey else { - return - } - - switch updated { - case .manual(_, let profile): - if let pic = profile.picture { - self.picture = pic + ZStack (alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { + InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight, disable_animation: disable_animation) + .onReceive(handle_notify(.profile_updated)) { updated in + guard updated.pubkey == self.pubkey else { + return } - case .remote(pubkey: let pk): - let profile_txn = profiles.lookup(id: pk) - let profile = profile_txn.unsafeUnownedValue - if let pic = profile?.picture { - self.picture = pic + + switch updated { + case .manual(_, let profile): + if let pic = profile.picture { + self.picture = pic + } + case .remote(pubkey: let pk): + let profile_txn = profiles.lookup(id: pk) + let profile = profile_txn.unsafeUnownedValue + if let pic = profile?.picture { + self.picture = pic + } } } + + if self.zappability_indicator, let lnurl = self.get_lnurl(), lnurl != "" { + Image("zap.fill") + .resizable() + .frame( + width: size * 0.24, + height: size * 0.24 + ) + .padding(size * 0.04) + .foregroundColor(.white) + .background(Color.orange) + .clipShape(Circle()) } + } } } diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index 583a82e4..7e8966b4 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -221,39 +221,13 @@ struct ProfileView: View { .accentColor(DamusColors.white) } - func lnButton(lnurl: String, unownedProfile: Profile?, pubkey: Pubkey) -> some View { - let reactions = unownedProfile?.reactions ?? true - let button_img = reactions ? "zap.fill" : "zap" - let lud16 = unownedProfile?.lud16 - - return Button(action: { [lnurl] in - present_sheet(.zap(target: .profile(self.profile.pubkey), lnurl: lnurl)) - }) { - Image(button_img) - .foregroundColor(button_img == "zap.fill" ? .orange : Color.primary) + func lnButton(unownedProfile: Profile?, record: ProfileRecord?) -> some View { + return ZapButtonView(unownedProfileRecord: record, profileModel: self.profile) { reactions_enabled, lud16, lnurl in + Image(reactions_enabled ? "zap.fill" : "zap") + .foregroundColor(reactions_enabled ? .orange : Color.primary) .profile_button_style(scheme: colorScheme) - .contextMenu { [lud16, reactions, lnurl] in - if reactions == false { - Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.") - } - - if let lud16 { - Button { - UIPasteboard.general.string = lud16 - } label: { - Label(lud16, image: "copy2") - } - } else { - Button { - UIPasteboard.general.string = lnurl - } label: { - Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), image: "copy") - } - } - } - + .cornerRadius(24) } - .cornerRadius(24) } var dmButton: some View { @@ -283,7 +257,7 @@ struct ProfileView: View { let lnurl = record.lnurl, lnurl != "" { - lnButton(lnurl: lnurl, unownedProfile: profile, pubkey: pubkey) + lnButton(unownedProfile: profile, record: record) } dmButton diff --git a/damus/Views/ProfileActionSheetView.swift b/damus/Views/ProfileActionSheetView.swift new file mode 100644 index 00000000..029e5f5c --- /dev/null +++ b/damus/Views/ProfileActionSheetView.swift @@ -0,0 +1,154 @@ +// +// ProfileActionSheetView.swift +// damus +// +// Created by Daniel D’Aquino on 2023-10-20. +// + +import SwiftUI + +struct ProfileActionSheetView: View { + let damus_state: DamusState + let pfp_size: CGFloat = 90.0 + + @StateObject var profile: ProfileModel + @StateObject var zap_button_model: ZapButtonModel = ZapButtonModel() + @State private var sheetHeight: CGFloat = .zero + + @Environment(\.dismiss) var dismiss + @Environment(\.colorScheme) var colorScheme + @Environment(\.presentationMode) var presentationMode + + init(damus_state: DamusState, pubkey: Pubkey) { + self.damus_state = damus_state + self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state)) + } + + func imageBorderColor() -> Color { + colorScheme == .light ? DamusColors.white : DamusColors.black + } + + func profile_data() -> ProfileRecord? { + let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey) + return profile_txn.unsafeUnownedValue + } + + func get_profile() -> Profile? { + return self.profile_data()?.profile + } + + var dmButton: some View { + let dm_model = damus_state.dms.lookup_or_create(profile.pubkey) + return VStack(alignment: .center, spacing: 10) { + Button( + action: { + damus_state.nav.push(route: Route.DMChat(dms: dm_model)) + dismiss() + }, + label: { + Image("messages") + .profile_button_style(scheme: colorScheme) + } + ) + .buttonStyle(NeutralCircleButtonStyle()) + Text(NSLocalizedString("Message", comment: "Button label that allows the user to start a direct message conversation with the user shown on-screen")) + .foregroundStyle(.secondary) + .font(.caption) + } + } + + var zapButton: some View { + if let lnurl = self.profile_data()?.lnurl, lnurl != "" { + return AnyView( + VStack(alignment: .center, spacing: 10) { + ZapButtonView(damus_state: damus_state, pubkey: self.profile.pubkey, action: { dismiss() }) { reactions_enabled, lud16, lnurl in + Image(reactions_enabled ? "zap.fill" : "zap") + .foregroundColor(reactions_enabled ? .orange : Color.primary) + .profile_button_style(scheme: colorScheme) + } + .buttonStyle(NeutralCircleButtonStyle()) + + Text(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")) + .foregroundStyle(.secondary) + .font(.caption) + } + ) + } + else { + return AnyView(EmptyView()) + } + } + + var profileName: some View { + let display_name = Profile.displayName(profile: self.get_profile(), pubkey: self.profile.pubkey).displayName + return HStack(alignment: .center, spacing: 10) { + Text(display_name) + .font(.title) + } + } + + var body: some View { + VStack(alignment: .center) { + ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) + if let url = self.profile_data()?.profile?.website_url { + WebsiteLink(url: url, style: .accent) + .padding(.top, -15) + } + + profileName + + PubkeyView(pubkey: profile.pubkey) + + if let about = self.profile_data()?.profile?.about { + AboutView(state: damus_state, about: about, max_about_length: 140, text_alignment: .center) + .padding(.top) + } + + HStack(spacing: 20) { + self.dmButton + self.zapButton + } + .padding() + + Button( + action: { + damus_state.nav.push(route: Route.ProfileByKey(pubkey: profile.pubkey)) + dismiss() + }, + label: { + HStack { + Spacer() + Text(NSLocalizedString("View full profile", comment: "A button label that allows the user to see the full profile of the profile they are previewing")) + Image(systemName: "arrow.up.right") + Spacer() + } + + } + ) + + .buttonStyle(NeutralCircleButtonStyle()) + } + .padding() + .padding(.top, 20) + .overlay { + GeometryReader { geometry in + Color.clear.preference(key: InnerHeightPreferenceKey.self, value: geometry.size.height) + } + } + .onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in + sheetHeight = newHeight + } + .presentationDetents([.height(sheetHeight)]) + } +} + +struct InnerHeightPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = .zero + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + +#Preview { + ProfileActionSheetView(damus_state: test_damus_state, pubkey: test_pubkey) +} diff --git a/damus/Views/PubkeyView.swift b/damus/Views/PubkeyView.swift index 2a4fcfae..6d31f4b0 100644 --- a/damus/Views/PubkeyView.swift +++ b/damus/Views/PubkeyView.swift @@ -7,6 +7,89 @@ import SwiftUI +struct PubkeyView: View { + let pubkey: Pubkey + + @Environment(\.colorScheme) var colorScheme + + @State private var isCopied = false + + func keyColor() -> Color { + colorScheme == .light ? DamusColors.black : DamusColors.white + } + + private func copyPubkey(_ pubkey: String) { + UIPasteboard.general.string = pubkey + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + withAnimation { + isCopied = true + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + withAnimation { + isCopied = false + } + } + } + } + + func pubkey_context_menu(pubkey: Pubkey) -> some View { + return self.contextMenu { + Button { + UIPasteboard.general.string = pubkey.npub + } label: { + Label(NSLocalizedString("Copy Account ID", comment: "Context menu option for copying the ID of the account that created the note."), image: "copy2") + } + } + } + + var body: some View { + let bech32 = pubkey.npub + + HStack { + Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))") + .font(.footnote) + .foregroundColor(keyColor()) + .padding(5) + .padding([.leading], 5) + + HStack { + if isCopied { + Image("check-circle") + .resizable() + .foregroundColor(DamusColors.green) + .frame(width: 20, height: 20) + Text(NSLocalizedString("Copied", comment: "Label indicating that a user's key was copied.")) + .font(.footnote) + .layoutPriority(1) + .foregroundColor(DamusColors.green) + } else { + Button { + copyPubkey(bech32) + } label: { + Label { + Text("Public key", comment: "Label indicating that the text is a user's public account key.") + } icon: { + Image("copy2") + .resizable() + .contentShape(Rectangle()) + .foregroundColor(colorScheme == .light ? DamusColors.darkGrey : DamusColors.lightGrey) + .frame(width: 20, height: 20) + } + .labelStyle(IconOnlyLabelStyle()) + .symbolRenderingMode(.hierarchical) + + } + } + } + .padding([.trailing], 10) + } + .background(RoundedRectangle(cornerRadius: 11).foregroundColor(colorScheme == .light ? DamusColors.adaptableGrey : DamusColors.neutral1)) + } +} + +#Preview { + PubkeyView(pubkey: test_pubkey) +} + func abbrev_pubkey(_ pubkey: String, amount: Int = 8) -> String { return pubkey.prefix(amount) + ":" + pubkey.suffix(amount) } diff --git a/damus/Views/Zaps/ZapButtonView.swift b/damus/Views/Zaps/ZapButtonView.swift new file mode 100644 index 00000000..07c91341 --- /dev/null +++ b/damus/Views/Zaps/ZapButtonView.swift @@ -0,0 +1,92 @@ +// +// ZapButtonView.swift +// damus +// +// Created by Daniel D’Aquino on 2023-10-20. +// + +import SwiftUI + +struct ZapButtonView: View { + typealias ContentViewFunction = (_ reactions_enabled: Bool, _ lud16: String?, _ lnurl: String?) -> Content + typealias ActionFunction = () -> Void + + let pubkey: Pubkey + @ViewBuilder let label: ContentViewFunction + let action: ActionFunction? + + let reactions_enabled: Bool + let lud16: String? + let lnurl: String? + + init(pubkey: Pubkey, reactions_enabled: Bool, lud16: String?, lnurl: String?, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { + self.pubkey = pubkey + self.label = label + self.action = action + self.reactions_enabled = reactions_enabled + self.lud16 = lud16 + self.lnurl = lnurl + } + + init(damus_state: DamusState, pubkey: Pubkey, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { + self.pubkey = pubkey + self.label = label + self.action = action + + let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey) + let record = profile_txn.unsafeUnownedValue + self.reactions_enabled = record?.profile?.reactions ?? true + self.lud16 = record?.profile?.lud06 + self.lnurl = record?.lnurl + } + + init(unownedProfileRecord: ProfileRecord?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { + self.pubkey = profileModel.pubkey + self.label = label + self.action = action + + self.reactions_enabled = unownedProfileRecord?.profile?.reactions ?? true + self.lud16 = unownedProfileRecord?.profile?.lud16 + self.lnurl = unownedProfileRecord?.lnurl + } + + var body: some View { + Button( + action: { + if let lnurl { + present_sheet(.zap(target: .profile(self.pubkey), lnurl: lnurl)) + } + action?() + }, + label: { + self.label(self.reactions_enabled, self.lud16, self.lnurl) + } + ) + .contextMenu { + if self.reactions_enabled == false { + Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.") + } + + if let lud16 { + Button { + UIPasteboard.general.string = lud16 + } label: { + Label(lud16, image: "copy2") + } + } else { + Button { + UIPasteboard.general.string = lnurl + } label: { + Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), image: "copy") + } + } + } + .disabled(lnurl == nil) + } +} + +#Preview { + ZapButtonView(pubkey: test_pubkey, reactions_enabled: true, lud16: make_test_profile().lud16, lnurl: "test@sendzaps.lol", label: { reactions_enabled, lud16, lnurl in + Image("zap.fill") + }) +}