diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index af675c898..5c988c4ea 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 3d61b19e4..f7aa797f4 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 51346f397..975d5810b 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 90063dd00..8bb23f534 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 edda8d50a..2ab1cd687 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 6d8071209..60525b133 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 948f86a22..905b8001d 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 0b4256e4d..62c123934 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 eb1821d76..44c585b4a 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 54afbbc53..779b7e843 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 583a82e48..7e8966b47 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 000000000..029e5f5c3 --- /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 2a4fcfaef..6d31f4b05 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 000000000..07c91341a --- /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") + }) +}