From e462dbd53c47eadfd2535af4c56acf25973a48fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 8 Jun 2026 09:08:46 -0700 Subject: [PATCH] Revert "Fix AttributeGraph cycle on Profile View" This reverts commit cba554b6ce9b8b0c481cfb34e7c5d3dc6db715ec. This commit was reverted because although it fixed one AttributeGraph cycle, it caused other aesthetic issues. The fix should be revisited to avoid AttributeGraph cycles. --- .../Features/Profile/Views/ProfileView.swift | 235 ++++++++---------- 1 file changed, 109 insertions(+), 126 deletions(-) diff --git a/damus/Features/Profile/Views/ProfileView.swift b/damus/Features/Profile/Views/ProfileView.swift index 4f67fa7e..29cae6c4 100644 --- a/damus/Features/Profile/Views/ProfileView.swift +++ b/damus/Features/Profile/Views/ProfileView.swift @@ -102,9 +102,7 @@ struct ProfileView: View { colorScheme == .light ? DamusColors.white : DamusColors.black } - /// Returns the blur opacity for the collapsing banner based on the current scroll offset and top safe-area inset. - func bannerBlurViewOpacity(topSafeAreaInset: CGFloat) -> Double { - let navbarHeight = navbarHeight(topSafeAreaInset: topSafeAreaInset) + func bannerBlurViewOpacity() -> Double { let progress = -(yOffset + navbarHeight) / 100 return Double(-yOffset > navbarHeight ? progress : 0) } @@ -116,9 +114,8 @@ struct ProfileView: View { return (displayName, "@\(userName)") } - /// Determines whether the follow button should appear in the collapsed banner state. - func showFollowBtnInBlurrBanner(topSafeAreaInset: CGFloat) -> Bool { - damus_state.contacts.follow_state(profile.pubkey) == .unfollows && bannerBlurViewOpacity(topSafeAreaInset: topSafeAreaInset) > 1.0 + func showFollowBtnInBlurrBanner() -> Bool { + damus_state.contacts.follow_state(profile.pubkey) == .unfollows && bannerBlurViewOpacity() > 1.0 } /// Builds the profile timeline filter, while ensuring the user's own profile bypasses NSFW and hashtag-spam content filters. @@ -136,12 +133,10 @@ struct ProfileView: View { return ContentFilters(filters: filters).filter } - /// Builds the stretching and collapsing banner while avoiding synchronous window safe-area queries on the main thread. - func bannerSection(topSafeAreaInset: CGFloat) -> some View { + var bannerSection: some View { GeometryReader { proxy -> AnyView in + let minY = proxy.frame(in: .global).minY - let navbarHeight = navbarHeight(topSafeAreaInset: topSafeAreaInset) - let blurOpacity = bannerBlurViewOpacity(topSafeAreaInset: topSafeAreaInset) DispatchQueue.main.async { self.yOffset = minY @@ -155,10 +150,10 @@ struct ProfileView: View { .frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight) .clipped() - VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)).opacity(blurOpacity) + VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)).opacity(bannerBlurViewOpacity()) } - Divider().opacity(blurOpacity) + Divider().opacity(bannerBlurViewOpacity()) } .frame(height: minY > 0 ? bannerHeight + minY : nil) .offset(y: minY > 0 ? -minY : -minY < navbarHeight ? 0 : -minY - navbarHeight) @@ -169,9 +164,8 @@ struct ProfileView: View { .allowsHitTesting(false) } - /// Computes the profile navigation height using the view's current safe-area context instead of querying the key window during SwiftUI updates. - func navbarHeight(topSafeAreaInset: CGFloat) -> CGFloat { - 100.0 - topSafeAreaInset + var navbarHeight: CGFloat { + return 100.0 - (Theme.safeAreaInsets?.top ?? 0) } func navImage(img: String) -> some View { @@ -305,30 +299,27 @@ struct ProfileView: View { } } - /// Returns the avatar's vertical offset during banner collapse. - func pfpOffset(topSafeAreaInset: CGFloat) -> CGFloat { - let progress = -yOffset / navbarHeight(topSafeAreaInset: topSafeAreaInset) + func pfpOffset() -> CGFloat { + let progress = -yOffset / navbarHeight let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1) return offset > 0 ? offset : 0 } - /// Returns the avatar's scale during banner collapse. - func pfpScale(topSafeAreaInset: CGFloat) -> CGFloat { - let progress = -yOffset / navbarHeight(topSafeAreaInset: topSafeAreaInset) + func pfpScale() -> CGFloat { + let progress = -yOffset / navbarHeight let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1)) return scale < 1 ? scale : 1 } - /// Builds the name and avatar section using geometry-provided safe-area values to avoid AttributeGraph cycles. - func nameSection(ndbprofile: Profile?, lnurl: String?, topSafeAreaInset: CGFloat) -> some View { + func nameSection(ndbprofile: Profile?, lnurl: String?) -> some View { return Group { let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey) HStack(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, damusState: damus_state) .padding(.top, -(pfp_size / 2.0)) - .offset(y: pfpOffset(topSafeAreaInset: topSafeAreaInset)) - .scaleEffect(pfpScale(topSafeAreaInset: topSafeAreaInset)) + .offset(y: pfpOffset()) + .scaleEffect(pfpScale()) .onTapGesture { is_zoomed.toggle() } @@ -366,13 +357,12 @@ struct ProfileView: View { } } - /// Builds the profile summary section using safe-area values from the surrounding geometry. - func aboutSection(topSafeAreaInset: CGFloat) -> some View { + var aboutSection: some View { VStack(alignment: .leading, spacing: 8.0) { let lnurl = try? damus_state.profiles.lookup_lnurl(profile.pubkey) let ndbprofile = try? damus_state.profiles.lookup(id: profile.pubkey) - nameSection(ndbprofile: ndbprofile, lnurl: lnurl, topSafeAreaInset: topSafeAreaInset) + nameSection(ndbprofile: ndbprofile, lnurl: lnurl) if let about = ndbprofile?.about { AboutView(state: damus_state, about: about) @@ -461,114 +451,107 @@ struct ProfileView: View { } var body: some View { - GeometryReader { geometry in - let safeAreaInsets = geometry.safeAreaInsets - let topSafeAreaInset = safeAreaInsets.top - let navbarHeight = navbarHeight(topSafeAreaInset: topSafeAreaInset) - let blurOpacity = bannerBlurViewOpacity(topSafeAreaInset: topSafeAreaInset) + ZStack { + ScrollView(.vertical) { + VStack(spacing: 0) { + bannerSection + .zIndex(1) + + VStack() { + aboutSection - ZStack { - ScrollView(.vertical) { - VStack(spacing: 0) { - bannerSection(topSafeAreaInset: topSafeAreaInset) - .zIndex(1) - - VStack { - aboutSection(topSafeAreaInset: topSafeAreaInset) - - VStack(spacing: 0) { - CustomPicker(tabs: tabs, selection: $filter_state) - Divider() - .frame(height: 1) - } - .background(colorScheme == .dark ? Color.black : Color.white) - - if filter_state == FilterState.posts { - InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts)) - } - if filter_state == FilterState.posts_and_replies { - InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies)) - } - if filter_state == FilterState.conversations && !profile.conversation_events.isEmpty { - InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.conversations)) - } + VStack(spacing: 0) { + CustomPicker(tabs: tabs, selection: $filter_state) + Divider() + .frame(height: 1) } - .padding(.horizontal, safeAreaInsets.leading) - .zIndex(-yOffset > navbarHeight ? 0 : 1) + .background(colorScheme == .dark ? Color.black : Color.white) + + if filter_state == FilterState.posts { + InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts)) + } + if filter_state == FilterState.posts_and_replies { + InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies)) + } + if filter_state == FilterState.conversations && !profile.conversation_events.isEmpty { + InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.conversations)) + } + } + .padding(.horizontal, Theme.safeAreaInsets?.left) + .zIndex(-yOffset > navbarHeight ? 0 : 1) + } + } + .padding(.bottom, tabHeight + getSafeAreaBottom()) + .ignoresSafeArea() + .navigationTitle("") + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .topBarLeading) { + HStack(spacing: 8) { + navBackButton + .padding(.top, 5) + .accentColor(DamusColors.white) + VStack(alignment: .leading, spacing: -4.5) { + Text(getProfileInfo().0) // Display name + .font(.headline) + .foregroundColor(.white) + Text(getProfileInfo().1) // Username + .font(.subheadline) + .foregroundColor(.white.opacity(0.8)) + } + .opacity(bannerBlurViewOpacity()) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, max(5, 15 + (yOffset / 30))) } } - .padding(.bottom, tabHeight + safeAreaInsets.bottom) - .ignoresSafeArea() - .navigationTitle("") - .navigationBarBackButtonHidden() - .toolbar { - ToolbarItem(placement: .topBarLeading) { - HStack(spacing: 8) { - navBackButton - .padding(.top, 5) - .accentColor(DamusColors.white) - VStack(alignment: .leading, spacing: -4.5) { - Text(getProfileInfo().0) // Display name - .font(.headline) - .foregroundColor(.white) - Text(getProfileInfo().1) // Username - .font(.subheadline) - .foregroundColor(.white.opacity(0.8)) - } - .opacity(blurOpacity) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, max(5, 15 + (yOffset / 30))) - } + .hideToolbarBackground() + + if showFollowBtnInBlurrBanner() { + ToolbarItem(placement: .topBarTrailing) { + FollowButtonView( + target: profile.get_follow_target(), + follows_you: profile.follows(pubkey: damus_state.pubkey), + follow_state: damus_state.contacts.follow_state(profile.pubkey) + ) + .padding(.top, 8) } .hideToolbarBackground() - - if showFollowBtnInBlurrBanner(topSafeAreaInset: topSafeAreaInset) { - ToolbarItem(placement: .topBarTrailing) { - FollowButtonView( - target: profile.get_follow_target(), - follows_you: profile.follows(pubkey: damus_state.pubkey), - follow_state: damus_state.contacts.follow_state(profile.pubkey) - ) - .padding(.top, 8) - } - .hideToolbarBackground() - } else { - ToolbarItem(placement: .topBarTrailing) { - navActionSheetButton - .padding(.top, 5) - .accentColor(DamusColors.white) - } - .hideToolbarBackground() + } else { + ToolbarItem(placement: .topBarTrailing) { + navActionSheetButton + .padding(.top, 5) + .accentColor(DamusColors.white) } + .hideToolbarBackground() } - .toolbarBackground(.hidden) - .onReceive(handle_notify(.switched_timeline)) { _ in - dismiss() - } - .onAppear() { - check_nip05_validity(pubkey: self.profile.pubkey, damus_state: self.damus_state) - profile.subscribe() - //followers.subscribe() - } - .onDisappear { - profile.unsubscribe() - followers.unsubscribe() - // our profilemodel needs a bit more help - } - .sheet(isPresented: $show_share_sheet) { - let url = URL(string: "https://damus.io/" + profile.pubkey.npub)! - ShareSheet(activityItems: [url]) - } - .damus_full_screen_cover($show_qr_code, damus_state: damus_state) { - QRCodeView(damus_state: damus_state, pubkey: profile.pubkey) - } + } + .toolbarBackground(.hidden) + .onReceive(handle_notify(.switched_timeline)) { _ in + dismiss() + } + .onAppear() { + check_nip05_validity(pubkey: self.profile.pubkey, damus_state: self.damus_state) + profile.subscribe() + //followers.subscribe() + } + .onDisappear { + profile.unsubscribe() + followers.unsubscribe() + // our profilemodel needs a bit more help + } + .sheet(isPresented: $show_share_sheet) { + let url = URL(string: "https://damus.io/" + profile.pubkey.npub)! + ShareSheet(activityItems: [url]) + } + .damus_full_screen_cover($show_qr_code, damus_state: damus_state) { + QRCodeView(damus_state: damus_state, pubkey: profile.pubkey) + } - if damus_state.is_privkey_user { - PostButtonContainer(is_left_handed: damus_state.settings.left_handed) { - notify(.compose(.posting(.user(profile.pubkey)))) - } - .padding(.bottom, tabHeight) + if damus_state.is_privkey_user { + PostButtonContainer(is_left_handed: damus_state.settings.left_handed) { + notify(.compose(.posting(.user(profile.pubkey)))) } + .padding(.bottom, tabHeight) } } }