From 504108da756f3ac6c9b037460bbbee2de64c4743 Mon Sep 17 00:00:00 2001 From: OlegAba Date: Thu, 9 Feb 2023 18:24:16 -0500 Subject: [PATCH] Add custom profile navbar --- damus/Views/ProfileView.swift | 389 ++++++++++++++++++---------------- 1 file changed, 212 insertions(+), 177 deletions(-) diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift index b68baedf..d047321a 100644 --- a/damus/Views/ProfileView.swift +++ b/damus/Views/ProfileView.swift @@ -80,9 +80,25 @@ struct EditButton: View { } } +struct VisualEffectView: UIViewRepresentable { + var effect: UIVisualEffect? + + func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { + UIVisualEffectView() + } + + func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { + uiView.effect = effect + } +} + struct ProfileView: View { let damus_state: DamusState - let zoom_size: CGFloat = 350 + let zoom_size: CGFloat = 350.0 + let pfp_size: CGFloat = 90.0 + let bannerHeight: CGFloat = 150.0 + + static let markdown = Markdown() @State private var selected_tab: ProfileTab = .posts @StateObject var profile: ProfileModel @@ -92,20 +108,12 @@ struct ProfileView: View { @State var is_zoomed: Bool = false @State var show_share_sheet: Bool = false @State var action_sheet_presented: Bool = false + @State var yOffset: CGFloat = 0 @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme @Environment(\.openURL) var openURL - - // We just want to have a white "< Home" text here, however, - // setting the initialiser is causing issues, and it's late. - // Ref: https://blog.techchee.com/navigation-bar-title-style-color-and-custom-back-button-in-swiftui/ - /* - init(damus_state: DamusState, zoom_size: CGFloat = 350) { - self.damus_state = damus_state - self.zoom_size = zoom_size - Theme.navigationBarColors(background: nil, titleColor: .white, tintColor: nil) - }*/ + @Environment(\.presentationMode) var presentationMode func fillColor() -> Color { colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey") @@ -115,7 +123,98 @@ struct ProfileView: View { colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack") } - func LNButton(lnurl: String, profile: Profile) -> some View { + func bannerBlurViewOpacity() -> Double { + let progress = -(yOffset + navbarHeight) / 100 + return Double(-yOffset > navbarHeight ? progress : 0) + } + + var bannerSection: some View { + GeometryReader { proxy -> AnyView in + + let minY = proxy.frame(in: .global).minY + + DispatchQueue.main.async { + self.yOffset = minY + } + + return AnyView( + VStack(spacing: 0) { + ZStack { + BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles) + .aspectRatio(contentMode: .fill) + .frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight) + .clipped() + + VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)).opacity(bannerBlurViewOpacity()) + } + + Divider().opacity(bannerBlurViewOpacity()) + } + .frame(height: minY > 0 ? bannerHeight + minY : nil) + .offset(y: minY > 0 ? -minY : -minY < navbarHeight ? 0 : -minY - navbarHeight) + ) + + } + .frame(height: bannerHeight) + } + + var navbarHeight: CGFloat { + return 100.0 - (Theme.safeAreaInsets?.top ?? 0) + } + + @ViewBuilder + func navImage(systemImage: String) -> some View { + Image(systemName: systemImage) + .frame(width: 33, height: 33) + .background(Color.black.opacity(0.6)) + .clipShape(Circle()) + } + + var navBackButton: some View { + Button { + presentationMode.wrappedValue.dismiss() + } label: { + navImage(systemImage: "chevron.left") + } + } + + var navActionSheetButton: some View { + Button(action: { + action_sheet_presented = true + }) { + navImage(systemImage: "ellipsis") + } + .confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) { + Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) { + show_share_sheet = true + } + + // Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile. + if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user { + Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) { + let target: ReportTarget = .user(profile.pubkey) + notify(.report, target) + } + + Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) { + notify(.block, profile.pubkey) + } + } + } + } + + var customNavbar: some View { + HStack { + navBackButton + Spacer() + navActionSheetButton + } + .padding(.top, 5) + .padding(.horizontal) + .accentColor(Color("DamusWhite")) + } + + func lnButton(lnurl: String, profile: Profile) -> some View { Button(action: { if damus_state.settings.show_wallet_selector { showing_select_wallet = true @@ -139,46 +238,8 @@ struct ProfileView: View { SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl) } } - - static let markdown = Markdown() - var ActionSheetButton: some View { - Button(action: { - action_sheet_presented = true - }) { - Image(systemName: "ellipsis.circle") - .profile_button_style(scheme: colorScheme) - } - .confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) { - Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) { - show_share_sheet = true - } - - // Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile. - if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user { - Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) { - let target: ReportTarget = .user(profile.pubkey) - notify(.report, target) - } - - Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) { - notify(.block, profile.pubkey) - } - } - } - - } - - var ShareButton: some View { - Button(action: { - show_share_sheet = true - }) { - Image(systemName: "square.and.arrow.up.circle") - .profile_button_style(scheme: colorScheme) - } - } - - var DMButton: some View { + var dmButton: some View { let dm_model = damus_state.dms.lookup_or_create(profile.pubkey) let dmview = DMChatView(damus_state: damus_state, pubkey: profile.pubkey) .environmentObject(dm_model) @@ -187,44 +248,17 @@ struct ProfileView: View { .profile_button_style(scheme: colorScheme) } } - - private func getScrollOffset(_ geometry: GeometryProxy) -> CGFloat { - geometry.frame(in: .global).minY - } - - private func getHeightForHeaderImage(_ geometry: GeometryProxy) -> CGFloat { - let offset = getScrollOffset(geometry) - let imageHeight = 150.0 - - if offset > 0 { - return imageHeight + offset - } - - return imageHeight - } - - private func getOffsetForHeaderImage(_ geometry: GeometryProxy) -> CGFloat { - let offset = getScrollOffset(geometry) - - // Image was pulled down - if offset > 0 { - return -offset - } - - return 0 - } - func ActionSection(profile_data: Profile?) -> some View { + func actionSection(profile_data: Profile?) -> some View { return Group { - ActionSheetButton if let profile = profile_data { if let lnurl = profile.lnurl, lnurl != "" { - LNButton(lnurl: lnurl, profile: profile) + lnButton(lnurl: lnurl, profile: profile) } } - DMButton + dmButton if profile.pubkey != damus_state.pubkey { FollowButtonView( @@ -241,110 +275,42 @@ struct ProfileView: View { } } - func NameSection(profile_data: Profile?) -> some View { + func pfpOffset() -> CGFloat { + let progress = -yOffset / navbarHeight + let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1) + return offset > 0 ? offset : 0 + } + + func pfpScale() -> CGFloat { + let progress = -yOffset / navbarHeight + let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1)) + return scale < 1 ? scale : 1 + } + + func nameSection(profile_data: Profile?) -> some View { return Group { HStack(alignment: .center) { ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles) + .padding(.top, -(pfp_size / 2.0)) + .offset(y: pfpOffset()) + .scaleEffect(pfpScale()) .onTapGesture { is_zoomed.toggle() } .fullScreenCover(isPresented: $is_zoomed) { ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) } - .offset(y: -(pfp_size/2.0)) // Increase if set a frame Spacer() - ActionSection(profile_data: profile_data) - .offset(y: -15.0) // Increase if set a frame + actionSection(profile_data: profile_data) } let follows_you = profile.follows(pubkey: damus_state.pubkey) ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state) - //.padding(.bottom) - .padding(.top,-(pfp_size/2.0)) } } - var pfp_size: CGFloat { - return 90.0 - } - - var TopSection: some View { - ZStack(alignment: .top) { - GeometryReader { geometry in - BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles) - .aspectRatio(contentMode: .fill) - .frame(width: geometry.size.width, height: self.getHeightForHeaderImage(geometry)) - .clipped() - .offset(x: 0, y: self.getOffsetForHeaderImage(geometry)) - - }.frame(height: BANNER_HEIGHT) - - VStack(alignment: .leading, spacing: 8.0) { - let profile_data = damus_state.profiles.lookup(id: profile.pubkey) - - NameSection(profile_data: profile_data) - - Text(ProfileView.markdown.process(profile_data?.about ?? "")) - .font(.subheadline).textSelection(.enabled) - - if let url = profile_data?.website_url { - WebsiteLink(url: url) - } - - Divider() - - HStack { - if let contact = profile.contacts { - let contacts = contact.referenced_pubkeys.map { $0.ref_id } - let following_model = FollowingModel(damus_state: damus_state, contacts: contacts) - NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) { - HStack { - Text("\(Text("\(profile.following)", comment: "Number of profiles a user is following.").font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.") - } - } - .buttonStyle(PlainButtonStyle()) - } - let fview = FollowersView(damus_state: damus_state, whos: profile.pubkey) - .environmentObject(followers) - if followers.contacts != nil { - NavigationLink(destination: fview) { - FollowersCount - } - .buttonStyle(PlainButtonStyle()) - } else { - FollowersCount - .onTapGesture { - UIImpactFeedbackGenerator(style: .light).impactOccurred() - followers.contacts = [] - followers.subscribe() - } - } - - if let relays = profile.relays { - // Only open relay config view if the user is logged in with private key and they are looking at their own profile. - let relay_text = Text("\(Text("\(relays.keys.count)", comment: "Number of relay servers a user is connected.").font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("relays_count", comment: "Part of a larger sentence to describe how many relay servers a user is connected."), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.") - if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user { - NavigationLink(destination: RelayConfigView(state: damus_state)) { - relay_text - } - .buttonStyle(PlainButtonStyle()) - } else { - NavigationLink(destination: UserRelaysView(state: damus_state, pubkey: profile.pubkey, relays: Array(relays.keys).sorted())) { - relay_text - } - .buttonStyle(PlainButtonStyle()) - } - } - } - } - .padding(.horizontal,18) - //.offset(y:120) - .padding(.top,150) - } - } - - var FollowersCount: some View { + var followersCount: some View { HStack { if followers.count == nil { Image(systemName: "square.and.arrow.down") @@ -357,20 +323,91 @@ struct ProfileView: View { } } } + + var aboutSection: some View { + VStack(alignment: .leading, spacing: 8.0) { + let profile_data = damus_state.profiles.lookup(id: profile.pubkey) + + nameSection(profile_data: profile_data) + + Text(ProfileView.markdown.process(profile_data?.about ?? "")) + .font(.subheadline).textSelection(.enabled) + + if let url = profile_data?.website_url { + WebsiteLink(url: url) + } + + Divider() + + HStack { + if let contact = profile.contacts { + let contacts = contact.referenced_pubkeys.map { $0.ref_id } + let following_model = FollowingModel(damus_state: damus_state, contacts: contacts) + NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) { + HStack { + Text("\(Text("\(profile.following)", comment: "Number of profiles a user is following.").font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.") + } + } + .buttonStyle(PlainButtonStyle()) + } + let fview = FollowersView(damus_state: damus_state, whos: profile.pubkey) + .environmentObject(followers) + if followers.contacts != nil { + NavigationLink(destination: fview) { + followersCount + } + .buttonStyle(PlainButtonStyle()) + } else { + followersCount + .onTapGesture { + UIImpactFeedbackGenerator(style: .light).impactOccurred() + followers.contacts = [] + followers.subscribe() + } + } + + if let relays = profile.relays { + // Only open relay config view if the user is logged in with private key and they are looking at their own profile. + let relay_text = Text("\(Text("\(relays.keys.count)", comment: "Number of relay servers a user is connected.").font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("relays_count", comment: "Part of a larger sentence to describe how many relay servers a user is connected."), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.") + if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user { + NavigationLink(destination: RelayConfigView(state: damus_state)) { + relay_text + } + .buttonStyle(PlainButtonStyle()) + } else { + NavigationLink(destination: UserRelaysView(state: damus_state, pubkey: profile.pubkey, relays: Array(relays.keys).sorted())) { + relay_text + } + .buttonStyle(PlainButtonStyle()) + } + } + } + } + .padding(.horizontal) + } var body: some View { - VStack(alignment: .leading) { - ScrollView { - TopSection - - Divider() + ScrollView(.vertical) { + VStack(spacing: 0) { + bannerSection + .zIndex(1) - InnerTimelineView(events: $profile.events, damus: damus_state, show_friend_icon: false, filter: { _ in true }) + VStack() { + aboutSection + + Divider() + + InnerTimelineView(events: $profile.events, damus: damus_state, show_friend_icon: false, filter: { _ in true }) + } + .padding(.horizontal, Theme.safeAreaInsets?.left) + .zIndex(-yOffset > navbarHeight ? 0 : 1) } - .frame(maxHeight: .infinity, alignment: .topLeading) } - .frame(maxWidth: .infinity, alignment: .topLeading) + .ignoresSafeArea() + .navigationTitle("") + .navigationBarHidden(true) + .overlay(customNavbar, alignment: .top) .onReceive(handle_notify(.switched_timeline)) { _ in dismiss() } @@ -390,7 +427,6 @@ struct ProfileView: View { } } } - .ignoresSafeArea() } } @@ -403,7 +439,6 @@ struct ProfileView_Previews: PreviewProvider { } } - func test_damus_state() -> DamusState { let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" let damus = DamusState.empty