Revert "Fix AttributeGraph cycle on Profile View"

This reverts commit cba554b6ce.

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.
This commit is contained in:
Daniel D’Aquino
2026-06-08 09:08:46 -07:00
parent bcd79652d8
commit e462dbd53c
+109 -126
View File
@@ -102,9 +102,7 @@ struct ProfileView: View {
colorScheme == .light ? DamusColors.white : DamusColors.black 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() -> Double {
func bannerBlurViewOpacity(topSafeAreaInset: CGFloat) -> Double {
let navbarHeight = navbarHeight(topSafeAreaInset: topSafeAreaInset)
let progress = -(yOffset + navbarHeight) / 100 let progress = -(yOffset + navbarHeight) / 100
return Double(-yOffset > navbarHeight ? progress : 0) return Double(-yOffset > navbarHeight ? progress : 0)
} }
@@ -116,9 +114,8 @@ struct ProfileView: View {
return (displayName, "@\(userName)") return (displayName, "@\(userName)")
} }
/// Determines whether the follow button should appear in the collapsed banner state. func showFollowBtnInBlurrBanner() -> Bool {
func showFollowBtnInBlurrBanner(topSafeAreaInset: CGFloat) -> Bool { damus_state.contacts.follow_state(profile.pubkey) == .unfollows && bannerBlurViewOpacity() > 1.0
damus_state.contacts.follow_state(profile.pubkey) == .unfollows && bannerBlurViewOpacity(topSafeAreaInset: topSafeAreaInset) > 1.0
} }
/// Builds the profile timeline filter, while ensuring the user's own profile bypasses NSFW and hashtag-spam content filters. /// 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 return ContentFilters(filters: filters).filter
} }
/// Builds the stretching and collapsing banner while avoiding synchronous window safe-area queries on the main thread. var bannerSection: some View {
func bannerSection(topSafeAreaInset: CGFloat) -> some View {
GeometryReader { proxy -> AnyView in GeometryReader { proxy -> AnyView in
let minY = proxy.frame(in: .global).minY let minY = proxy.frame(in: .global).minY
let navbarHeight = navbarHeight(topSafeAreaInset: topSafeAreaInset)
let blurOpacity = bannerBlurViewOpacity(topSafeAreaInset: topSafeAreaInset)
DispatchQueue.main.async { DispatchQueue.main.async {
self.yOffset = minY self.yOffset = minY
@@ -155,10 +150,10 @@ struct ProfileView: View {
.frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight) .frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight)
.clipped() .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) .frame(height: minY > 0 ? bannerHeight + minY : nil)
.offset(y: minY > 0 ? -minY : -minY < navbarHeight ? 0 : -minY - navbarHeight) .offset(y: minY > 0 ? -minY : -minY < navbarHeight ? 0 : -minY - navbarHeight)
@@ -169,9 +164,8 @@ struct ProfileView: View {
.allowsHitTesting(false) .allowsHitTesting(false)
} }
/// Computes the profile navigation height using the view's current safe-area context instead of querying the key window during SwiftUI updates. var navbarHeight: CGFloat {
func navbarHeight(topSafeAreaInset: CGFloat) -> CGFloat { return 100.0 - (Theme.safeAreaInsets?.top ?? 0)
100.0 - topSafeAreaInset
} }
func navImage(img: String) -> some View { func navImage(img: String) -> some View {
@@ -305,30 +299,27 @@ struct ProfileView: View {
} }
} }
/// Returns the avatar's vertical offset during banner collapse. func pfpOffset() -> CGFloat {
func pfpOffset(topSafeAreaInset: CGFloat) -> CGFloat { let progress = -yOffset / navbarHeight
let progress = -yOffset / navbarHeight(topSafeAreaInset: topSafeAreaInset)
let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1) let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1)
return offset > 0 ? offset : 0 return offset > 0 ? offset : 0
} }
/// Returns the avatar's scale during banner collapse. func pfpScale() -> CGFloat {
func pfpScale(topSafeAreaInset: CGFloat) -> CGFloat { let progress = -yOffset / navbarHeight
let progress = -yOffset / navbarHeight(topSafeAreaInset: topSafeAreaInset)
let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1)) let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1))
return scale < 1 ? scale : 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?) -> some View {
func nameSection(ndbprofile: Profile?, lnurl: String?, topSafeAreaInset: CGFloat) -> some View {
return Group { return Group {
let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey) let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey)
HStack(alignment: .center) { 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) 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)) .padding(.top, -(pfp_size / 2.0))
.offset(y: pfpOffset(topSafeAreaInset: topSafeAreaInset)) .offset(y: pfpOffset())
.scaleEffect(pfpScale(topSafeAreaInset: topSafeAreaInset)) .scaleEffect(pfpScale())
.onTapGesture { .onTapGesture {
is_zoomed.toggle() is_zoomed.toggle()
} }
@@ -366,13 +357,12 @@ struct ProfileView: View {
} }
} }
/// Builds the profile summary section using safe-area values from the surrounding geometry. var aboutSection: some View {
func aboutSection(topSafeAreaInset: CGFloat) -> some View {
VStack(alignment: .leading, spacing: 8.0) { VStack(alignment: .leading, spacing: 8.0) {
let lnurl = try? damus_state.profiles.lookup_lnurl(profile.pubkey) let lnurl = try? damus_state.profiles.lookup_lnurl(profile.pubkey)
let ndbprofile = try? damus_state.profiles.lookup(id: 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 { if let about = ndbprofile?.about {
AboutView(state: damus_state, about: about) AboutView(state: damus_state, about: about)
@@ -461,114 +451,107 @@ struct ProfileView: View {
} }
var body: some View { var body: some View {
GeometryReader { geometry in ZStack {
let safeAreaInsets = geometry.safeAreaInsets ScrollView(.vertical) {
let topSafeAreaInset = safeAreaInsets.top VStack(spacing: 0) {
let navbarHeight = navbarHeight(topSafeAreaInset: topSafeAreaInset) bannerSection
let blurOpacity = bannerBlurViewOpacity(topSafeAreaInset: topSafeAreaInset) .zIndex(1)
VStack() {
aboutSection
ZStack { VStack(spacing: 0) {
ScrollView(.vertical) { CustomPicker(tabs: tabs, selection: $filter_state)
VStack(spacing: 0) { Divider()
bannerSection(topSafeAreaInset: topSafeAreaInset) .frame(height: 1)
.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))
}
} }
.padding(.horizontal, safeAreaInsets.leading) .background(colorScheme == .dark ? Color.black : Color.white)
.zIndex(-yOffset > navbarHeight ? 0 : 1)
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) .hideToolbarBackground()
.ignoresSafeArea()
.navigationTitle("") if showFollowBtnInBlurrBanner() {
.navigationBarBackButtonHidden() ToolbarItem(placement: .topBarTrailing) {
.toolbar { FollowButtonView(
ToolbarItem(placement: .topBarLeading) { target: profile.get_follow_target(),
HStack(spacing: 8) { follows_you: profile.follows(pubkey: damus_state.pubkey),
navBackButton follow_state: damus_state.contacts.follow_state(profile.pubkey)
.padding(.top, 5) )
.accentColor(DamusColors.white) .padding(.top, 8)
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() .hideToolbarBackground()
} else {
if showFollowBtnInBlurrBanner(topSafeAreaInset: topSafeAreaInset) { ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .topBarTrailing) { navActionSheetButton
FollowButtonView( .padding(.top, 5)
target: profile.get_follow_target(), .accentColor(DamusColors.white)
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()
} }
.hideToolbarBackground()
} }
.toolbarBackground(.hidden) }
.onReceive(handle_notify(.switched_timeline)) { _ in .toolbarBackground(.hidden)
dismiss() .onReceive(handle_notify(.switched_timeline)) { _ in
} dismiss()
.onAppear() { }
check_nip05_validity(pubkey: self.profile.pubkey, damus_state: self.damus_state) .onAppear() {
profile.subscribe() check_nip05_validity(pubkey: self.profile.pubkey, damus_state: self.damus_state)
//followers.subscribe() profile.subscribe()
} //followers.subscribe()
.onDisappear { }
profile.unsubscribe() .onDisappear {
followers.unsubscribe() profile.unsubscribe()
// our profilemodel needs a bit more help followers.unsubscribe()
} // our profilemodel needs a bit more help
.sheet(isPresented: $show_share_sheet) { }
let url = URL(string: "https://damus.io/" + profile.pubkey.npub)! .sheet(isPresented: $show_share_sheet) {
ShareSheet(activityItems: [url]) 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) .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 { if damus_state.is_privkey_user {
PostButtonContainer(is_left_handed: damus_state.settings.left_handed) { PostButtonContainer(is_left_handed: damus_state.settings.left_handed) {
notify(.compose(.posting(.user(profile.pubkey)))) notify(.compose(.posting(.user(profile.pubkey))))
}
.padding(.bottom, tabHeight)
} }
.padding(.bottom, tabHeight)
} }
} }
} }