From 6751bc15cc5d07c72f2c52f113c89c5ec1b0798c Mon Sep 17 00:00:00 2001 From: ericholguin Date: Fri, 6 Sep 2024 09:12:16 -0600 Subject: [PATCH] ux: Seamless Timeline This PR is a ux change to make the header, tabbar, and post button disappear when the user scrolls. The main tabbar is now an overlay which means it will display over views, this was needed in order to get the timeline to extend behind it. However, this mean we must add bottom padding to any view where the main tabbar is present to account for the overlap. Changelog-Added: Disappearing header, tabbar, and post button on scroll Signed-off-by: ericholguin --- damus.xcodeproj/project.pbxproj | 10 +- damus/Components/CustomPicker.swift | 1 - damus/ContentView.swift | 56 +++++++----- damus/Util/Extensions/OffsetExtension.swift | 78 ++++++++++++++++ damus/Views/BookmarksView.swift | 1 + damus/Views/Chat/ChatroomThreadView.swift | 3 + damus/Views/ConfigView.swift | 5 +- damus/Views/MainTabView.swift | 3 + damus/Views/Muting/MutelistView.swift | 5 +- damus/Views/Profile/EditMetadataView.swift | 2 +- damus/Views/Profile/ProfileView.swift | 2 + damus/Views/ReactionsView.swift | 1 + damus/Views/Relays/SignalView.swift | 15 ++- damus/Views/RepostsView.swift | 1 + .../Settings/AppearanceSettingsView.swift | 1 + .../Settings/NotificationSettingsView.swift | 5 +- damus/Views/SideMenuView.swift | 5 +- .../Views/Timeline/PostingTimelineView.swift | 91 ++++++++++++++++--- damus/Views/TimelineView.swift | 56 ++++++++++-- damus/Views/Zaps/ZapsView.swift | 1 + 20 files changed, 280 insertions(+), 62 deletions(-) create mode 100644 damus/Util/Extensions/OffsetExtension.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index ad1e5d4f..58ee41ac 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -351,7 +351,6 @@ 4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; }; 4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8795A2996C47A00F758CC /* ZapsModel.swift */; }; 4CE9FBBA2A6B3C63007E485C /* nostrdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CE9FBB82A6B3B26007E485C /* nostrdb.c */; settings = {COMPILER_FLAGS = "-w"; }; }; - 4CED18FD2C84B28F006AF665 /* PostingTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */; }; 4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; }; 4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */; }; 4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */; }; @@ -395,6 +394,9 @@ 50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; }; 50C3E08A2AA8E3F7006A4BC0 /* AVPlayer+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */; }; 50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; }; + 5C0567532C8B5F9C0073F23A /* PostingTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */; }; + 5C0567552C8B60C20073F23A /* OffsetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0567542C8B60C20073F23A /* OffsetExtension.swift */; }; + 5C0567562C8B60E60073F23A /* OffsetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0567542C8B60C20073F23A /* OffsetExtension.swift */; }; 5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; }; 5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */; }; 5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */; }; @@ -1834,6 +1836,7 @@ 50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = ""; }; 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVPlayer+Additions.swift"; sourceTree = ""; }; 50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = ""; }; + 5C0567542C8B60C20073F23A /* OffsetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsetExtension.swift; sourceTree = ""; }; 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = ""; }; 5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySoftwareDetail.swift; sourceTree = ""; }; 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayAdminDetail.swift; sourceTree = ""; }; @@ -3263,6 +3266,7 @@ 4C7D09752A0AF19E00943473 /* FillAndStroke.swift */, D72E12772BEED22400F4F781 /* Array.swift */, D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */, + 5C0567542C8B60C20073F23A /* OffsetExtension.swift */, ); path = Extensions; sourceTree = ""; @@ -3802,6 +3806,7 @@ 50C3E08A2AA8E3F7006A4BC0 /* AVPlayer+Additions.swift in Sources */, 4C198DF229F88C6B004C165C /* BlurHashDecode.swift in Sources */, F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */, + 5C0567552C8B60C20073F23A /* OffsetExtension.swift in Sources */, 4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */, 4C7FF7D52823313F009601DB /* Mentions.swift in Sources */, 4C32B94D2A9AD44700DC3548 /* Offset.swift in Sources */, @@ -4257,7 +4262,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4CED18FD2C84B28F006AF665 /* PostingTimelineView.swift in Sources */, D73E5E202C6A97F4007EB227 /* AttachedWalletNotify.swift in Sources */, D73E5E212C6A97F4007EB227 /* DisplayTabBarNotify.swift in Sources */, D73E5E222C6A97F4007EB227 /* BroadcastNotify.swift in Sources */, @@ -4448,6 +4452,7 @@ D73E5EDD2C6A97F4007EB227 /* NotificationSettingsView.swift in Sources */, D73E5EDE2C6A97F4007EB227 /* AppearanceSettingsView.swift in Sources */, D73E5EDF2C6A97F4007EB227 /* KeySettingsView.swift in Sources */, + 5C0567562C8B60E60073F23A /* OffsetExtension.swift in Sources */, D73E5EE02C6A97F4007EB227 /* ZapSettingsView.swift in Sources */, D73E5F792C6A9C4C007EB227 /* HomeModel.swift in Sources */, D73E5EE12C6A97F4007EB227 /* TranslationSettingsView.swift in Sources */, @@ -4521,6 +4526,7 @@ D73E5F242C6A97F4007EB227 /* HighlightLink.swift in Sources */, D73E5F252C6A97F4007EB227 /* HighlightEventRef.swift in Sources */, D73E5F262C6A97F4007EB227 /* HighlightDraftContentView.swift in Sources */, + 5C0567532C8B5F9C0073F23A /* PostingTimelineView.swift in Sources */, D73E5F272C6A97F4007EB227 /* TimeDot.swift in Sources */, D73E5F282C6A97F4007EB227 /* EventTop.swift in Sources */, D73E5F292C6A97F4007EB227 /* ReplyDescription.swift in Sources */, diff --git a/damus/Components/CustomPicker.swift b/damus/Components/CustomPicker.swift index 454384d0..6d178974 100644 --- a/damus/Components/CustomPicker.swift +++ b/damus/Components/CustomPicker.swift @@ -46,7 +46,6 @@ struct CustomPicker: View { .accentColor(tag == selection ? textColor() : .gray) } } - .background(Color(UIColor.systemBackground)) } func textColor() -> Color { diff --git a/damus/ContentView.swift b/damus/ContentView.swift index b10ad11d..515e13f6 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -61,6 +61,8 @@ func present_sheet(_ sheet: Sheets) { notify(.present_sheet(sheet)) } +var tabHeight: CGFloat = 0.0 + struct ContentView: View { let keypair: Keypair let appDelegate: AppDelegate? @@ -89,6 +91,7 @@ struct ContentView: View { @State var user_muted_confirm: Bool = false @State var confirm_overwrite_mutelist: Bool = false @State private var isSideBarOpened = false + @State var headerOffset: CGFloat = 0.0 var home: HomeModel = HomeModel() @StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator() @AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false @@ -131,7 +134,7 @@ struct ContentView: View { } case .home: - PostingTimelineView(damus_state: damus_state!, home: home, active_sheet: $active_sheet) + PostingTimelineView(damus_state: damus_state!, home: home, isSideBarOpened: $isSideBarOpened, active_sheet: $active_sheet, headerOffset: $headerOffset) case .notifications: NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle) @@ -140,25 +143,16 @@ struct ContentView: View { DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings) } } + .background(DamusColors.adaptableWhite) + .edgesIgnoringSafeArea(selected_timeline != .home ? [] : [.top, .bottom]) .navigationBarTitle(timeline_name(selected_timeline), displayMode: .inline) + .toolbar(selected_timeline != .home ? .visible : .hidden) .toolbar { ToolbarItem(placement: .principal) { VStack { - if selected_timeline == .home { - Image("damus-home") - .resizable() - .frame(width:30,height:30) - .shadow(color: DamusColors.purple, radius: 2) - .opacity(isSideBarOpened ? 0 : 1) - .animation(isSideBarOpened ? .none : .default, value: isSideBarOpened) - .onTapGesture { - isSideBarOpened.toggle() - } - } else { - timelineNavItem - .opacity(isSideBarOpened ? 0 : 1) - .animation(isSideBarOpened ? .none : .default, value: isSideBarOpened) - } + timelineNavItem + .opacity(isSideBarOpened ? 0 : 1) + .animation(isSideBarOpened ? .none : .default, value: isSideBarOpened) } } } @@ -237,9 +231,11 @@ struct ContentView: View { } } } + .background(DamusColors.adaptableWhite) + .edgesIgnoringSafeArea(selected_timeline != .home ? [] : [.top, .bottom]) .tabViewStyle(.page(indexDisplayMode: .never)) .overlay( - SideMenuView(damus_state: damus_state!, isSidebarVisible: $isSideBarOpened.animation()) + SideMenuView(damus_state: damus_state!, isSidebarVisible: $isSideBarOpened.animation(), selected: $selected_timeline) ) .navigationDestination(for: Route.self) { route in route.view(navigationCoordinator: navigationCoordinator, damusState: damus_state!) @@ -249,13 +245,25 @@ struct ContentView: View { } } .navigationViewStyle(.stack) - - if !hide_bar { - TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline) - .padding([.bottom], 8) - .background(Color(uiColor: .systemBackground).ignoresSafeArea()) - } else { - Text("") + .overlay(alignment: .bottom) { + if !hide_bar { + if !isSideBarOpened { + TabBar(nstatus: home.notification_status, navIsAtRoot: navIsAtRoot(), selected: $selected_timeline, headerOffset: $headerOffset, settings: damus.settings, action: switch_timeline) + .padding([.bottom], 8) + .background(selected_timeline != .home || (selected_timeline == .home && !self.navIsAtRoot()) ? DamusColors.adaptableWhite : DamusColors.adaptableWhite.opacity(abs(1.25 - (abs(headerOffset/100.0))))) + .anchorPreference(key: HeaderBoundsKey.self, value: .bounds){$0} + .overlayPreferenceValue(HeaderBoundsKey.self) { value in + GeometryReader{ proxy in + if let anchor = value{ + Color.clear + .onAppear { + tabHeight = proxy[anchor].height + } + } + } + } + } + } } } } diff --git a/damus/Util/Extensions/OffsetExtension.swift b/damus/Util/Extensions/OffsetExtension.swift new file mode 100644 index 00000000..266e20ab --- /dev/null +++ b/damus/Util/Extensions/OffsetExtension.swift @@ -0,0 +1,78 @@ +// +// OffsetExtension.swift +// damus +// +// Created by eric on 9/6/24. +// + +import SwiftUI + +enum SwipeDirection { + case up + case down + case none +} + +extension View { + @ViewBuilder + func offsetY(completion: @escaping (CGFloat, CGFloat)->())->some View { + self + .modifier(OffsetHelper(onChange: completion)) + } + + func safeArea() -> UIEdgeInsets { + guard let scene = this_app.connectedScenes.first as? UIWindowScene else{return .zero} + guard let safeArea = scene.windows.first?.safeAreaInsets else{return .zero} + return safeArea + } +} + +struct OffsetHelper: ViewModifier{ + var onChange: (CGFloat,CGFloat)->() + @State var currentOffset: CGFloat = 0 + @State var previousOffset: CGFloat = 0 + + func body(content: Content) -> some View { + content + .overlay { + GeometryReader{proxy in + let minY = proxy.frame(in: .named("scroll")).minY + Color.clear + .preference(key: OffsetKey.self, value: minY) + .onPreferenceChange(OffsetKey.self) { value in + previousOffset = currentOffset + currentOffset = value + onChange(previousOffset,currentOffset) + } + } + } + } +} + +struct OffsetKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + +struct HeaderBoundsKey: PreferenceKey{ + static var defaultValue: Anchor? + + static func reduce(value: inout Anchor?, nextValue: () -> Anchor?) { + value = nextValue() + } +} + +func getSafeAreaTop()->CGFloat{ + guard let scene = this_app.connectedScenes.first as? UIWindowScene else{return .zero} + guard let topSafeArea = scene.windows.first?.safeAreaInsets.top else{return .zero} + return topSafeArea +} + +func getSafeAreaBottom()->CGFloat{ + guard let scene = this_app.connectedScenes.first as? UIWindowScene else{return .zero} + guard let bottomSafeArea = scene.windows.first?.safeAreaInsets.bottom else{return .zero} + return bottomSafeArea +} diff --git a/damus/Views/BookmarksView.swift b/damus/Views/BookmarksView.swift index f7fb9074..2b196b91 100644 --- a/damus/Views/BookmarksView.swift +++ b/damus/Views/BookmarksView.swift @@ -39,6 +39,7 @@ struct BookmarksView: View { ScrollView { InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, filter: noneFilter) } + .padding(.bottom, 10 + tabHeight + getSafeAreaBottom()) } } .onReceive(handle_notify(.switched_timeline)) { _ in diff --git a/damus/Views/Chat/ChatroomThreadView.swift b/damus/Views/Chat/ChatroomThreadView.swift index 16594929..4113b1bd 100644 --- a/damus/Views/Chat/ChatroomThreadView.swift +++ b/damus/Views/Chat/ChatroomThreadView.swift @@ -135,6 +135,9 @@ struct ChatroomThreadView: View { } .padding(.top) EndBlock() + + HStack {} + .frame(height: tabHeight + getSafeAreaBottom()) } .onReceive(handle_notify(.post), perform: { notify in switch notify { diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift index c273ddd4..42f99381 100644 --- a/damus/Views/ConfigView.swift +++ b/damus/Views/ConfigView.swift @@ -99,7 +99,10 @@ struct ConfigView: View { } } - Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) { + Section( + header: Text(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")), + footer: Text("").padding(.bottom, tabHeight + getSafeAreaBottom()) + ) { Text(verbatim: VersionInfo.version) .contextMenu { Button { diff --git a/damus/Views/MainTabView.swift b/damus/Views/MainTabView.swift index f07b674f..f258fa8d 100644 --- a/damus/Views/MainTabView.swift +++ b/damus/Views/MainTabView.swift @@ -66,7 +66,9 @@ struct TabButton: View { struct TabBar: View { var nstatus: NotificationStatusModel + var navIsAtRoot: Bool @Binding var selected: Timeline + @Binding var headerOffset: CGFloat let settings: UserSettingsStore let action: (Timeline) -> () @@ -81,5 +83,6 @@ struct TabBar: View { TabButton(timeline: .notifications, img: "notification-bell", selected: $selected, nstatus: nstatus, settings: settings, action: action).keyboardShortcut("4") } } + .opacity(selected != .home || (selected == .home && !navIsAtRoot) ? 1.0 : (abs(1.25 - (abs(headerOffset/100.0))))) } } diff --git a/damus/Views/Muting/MutelistView.swift b/damus/Views/Muting/MutelistView.swift index 1a9548fa..b9ef3371 100644 --- a/damus/Views/Muting/MutelistView.swift +++ b/damus/Views/Muting/MutelistView.swift @@ -86,7 +86,10 @@ struct MutelistView: View { } } } - Section(NSLocalizedString("Threads", comment: "Section header title for a list of threads that are muted.")) { + Section( + header: Text(NSLocalizedString("Threads", comment: "Section header title for a list of threads that are muted.")), + footer: Text("").padding(.bottom, 10 + tabHeight + getSafeAreaBottom()) + ) { ForEach(threads, id: \.self) { item in if case let MuteItem.thread(note_id, _) = item { if let event = damus_state.events.lookup(note_id) { diff --git a/damus/Views/Profile/EditMetadataView.swift b/damus/Views/Profile/EditMetadataView.swift index dc982175..7f01b4c7 100644 --- a/damus/Views/Profile/EditMetadataView.swift +++ b/damus/Views/Profile/EditMetadataView.swift @@ -203,7 +203,7 @@ struct EditMetadataView: View { }) .buttonStyle(GradientButtonStyle(padding: 15)) .padding(.horizontal, 10) - .padding(.bottom, 10) + .padding(.bottom, 10 + tabHeight + getSafeAreaBottom()) .disabled(!didChange()) .opacity(!didChange() ? 0.5 : 1) .disabled(profileUploadObserver.isLoading || bannerUploadObserver.isLoading) diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index a17df5f0..013cd05c 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -444,6 +444,7 @@ struct ProfileView: View { .zIndex(-yOffset > navbarHeight ? 0 : 1) } } + .padding(.bottom, tabHeight + getSafeAreaBottom()) .ignoresSafeArea() .navigationTitle("") .navigationBarBackButtonHidden() @@ -485,6 +486,7 @@ struct ProfileView: View { PostButtonContainer(is_left_handed: damus_state.settings.left_handed) { notify(.compose(.posting(.user(profile.pubkey)))) } + .padding(.bottom, tabHeight) } } } diff --git a/damus/Views/ReactionsView.swift b/damus/Views/ReactionsView.swift index c39ff56f..242e8ba4 100644 --- a/damus/Views/ReactionsView.swift +++ b/damus/Views/ReactionsView.swift @@ -22,6 +22,7 @@ struct ReactionsView: View { } .padding() } + .padding(.bottom, tabHeight + getSafeAreaBottom()) .navigationBarTitle(NSLocalizedString("Reactions", comment: "Navigation bar title for Reactions view.")) .onAppear { model.subscribe() diff --git a/damus/Views/Relays/SignalView.swift b/damus/Views/Relays/SignalView.swift index 43a92784..629d5a8d 100644 --- a/damus/Views/Relays/SignalView.swift +++ b/damus/Views/Relays/SignalView.swift @@ -13,15 +13,14 @@ struct SignalView: View { var body: some View { Group { - if signal.signal != signal.max_signal { - NavigationLink(value: Route.RelayConfig) { - Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.") - .font(.callout) - .foregroundColor(.gray) - } - } else { - Text("") + NavigationLink(value: Route.RelayConfig) { + Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.") + .font(.callout) + .foregroundColor(.gray) } + .frame(width:50,height:30) + .opacity(signal.signal != signal.max_signal ? 1 : 0) + .disabled(signal.signal == signal.max_signal) } } diff --git a/damus/Views/RepostsView.swift b/damus/Views/RepostsView.swift index 368f11ce..c48f43fe 100644 --- a/damus/Views/RepostsView.swift +++ b/damus/Views/RepostsView.swift @@ -20,6 +20,7 @@ struct RepostsView: View { } .padding() } + .padding(.bottom, tabHeight + getSafeAreaBottom()) .navigationBarTitle(NSLocalizedString("Reposts", comment: "Navigation bar title for Reposts view.")) .onAppear { model.subscribe() diff --git a/damus/Views/Settings/AppearanceSettingsView.swift b/damus/Views/Settings/AppearanceSettingsView.swift index 24403c73..d5a4c1b6 100644 --- a/damus/Views/Settings/AppearanceSettingsView.swift +++ b/damus/Views/Settings/AppearanceSettingsView.swift @@ -108,6 +108,7 @@ struct AppearanceSettingsView: View { Section( header: Text("Profiles", comment: "Section title for profile view configuration."), footer: Text("Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile", comment: "Section footer clarifying what the profile action sheet feature does") + .padding(.bottom, tabHeight + getSafeAreaBottom()) ) { Toggle(NSLocalizedString("Show profile action sheets", comment: "Setting to show profile action sheets when clicking on a user's profile picture"), isOn: $settings.show_profile_action_sheet_on_pfp_click) .toggleStyle(.switch) diff --git a/damus/Views/Settings/NotificationSettingsView.swift b/damus/Views/Settings/NotificationSettingsView.swift index e5185242..f08866d2 100644 --- a/damus/Views/Settings/NotificationSettingsView.swift +++ b/damus/Views/Settings/NotificationSettingsView.swift @@ -177,7 +177,10 @@ struct NotificationSettingsView: View { .toggleStyle(.switch) } - Section(header: Text("Notification Dots", comment: "Section header for notification indicator dot settings")) { + Section( + header: Text("Notification Dots", comment: "Section header for notification indicator dot settings"), + footer: Text("").padding(.bottom, tabHeight + getSafeAreaBottom()) + ) { Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: indicator_binding(.zaps)) .toggleStyle(.switch) Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: indicator_binding(.mentions)) diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift index 9d801dfc..04ee8305 100644 --- a/damus/Views/SideMenuView.swift +++ b/damus/Views/SideMenuView.swift @@ -11,6 +11,7 @@ import SwiftUI struct SideMenuView: View { let damus_state: DamusState @Binding var isSidebarVisible: Bool + @Binding var selected: Timeline @State var confirm_logout: Bool = false @State private var showQRCode = false @@ -200,7 +201,7 @@ struct SideMenuView: View { } .padding(.top, verticalSpacing) } - .padding(.top, -(padding / 2.0)) + .padding(.top, selected != .home ? -(padding / 2.0) : 30) .padding([.leading, .trailing, .bottom], padding) } .frame(width: sideBarWidth) @@ -249,6 +250,6 @@ struct SideMenuView: View { struct Previews_SideMenuView_Previews: PreviewProvider { static var previews: some View { let ds = test_damus_state - SideMenuView(damus_state: ds, isSidebarVisible: .constant(true)) + SideMenuView(damus_state: ds, isSidebarVisible: .constant(true), selected: .constant(.home)) } } diff --git a/damus/Views/Timeline/PostingTimelineView.swift b/damus/Views/Timeline/PostingTimelineView.swift index 3b7a515a..0ec55cea 100644 --- a/damus/Views/Timeline/PostingTimelineView.swift +++ b/damus/Views/Timeline/PostingTimelineView.swift @@ -16,11 +16,14 @@ struct PostingTimelineView: View { @State var initialOffset: CGFloat? @State var offset: CGFloat? @State var showSearch: Bool = true + @Binding var isSideBarOpened: Bool @Binding var active_sheet: Sheets? @FocusState private var isSearchFocused: Bool @State private var contentOffset: CGFloat = 0 @State private var indicatorWidth: CGFloat = 0 @State private var indicatorPosition: CGFloat = 0 + @State var headerHeight: CGFloat = 0 + @Binding var headerOffset: CGFloat @SceneStorage("PostingTimelineView.filter_state") var filter_state : FilterState = .posts_and_replies var mystery: some View { @@ -35,8 +38,63 @@ struct PostingTimelineView: View { } func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View { - TimelineView(events: home.events, loading: .constant(false), damus: damus_state, show_friend_icon: false, filter: filter) { - PullDownSearchView(state: damus_state, on_cancel: {}) + TimelineView(events: home.events, loading: .constant(false), headerHeight: $headerHeight, headerOffset: $headerOffset, damus: damus_state, show_friend_icon: false, filter: filter) + } + + func HeaderView()->some View { + VStack { + VStack(spacing: 0) { + // This is needed for the Dynamic Island + HStack {} + .frame(height: getSafeAreaTop()) + + HStack(alignment: .top) { + Button { + isSideBarOpened.toggle() + } label: { + ProfilePicView(pubkey: damus_state.pubkey, size: 32, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) + .opacity(isSideBarOpened ? 0 : 1) + .animation(isSideBarOpened ? .none : .default, value: isSideBarOpened) + } + .disabled(isSideBarOpened) + + Spacer() + + Image("damus-home") + .resizable() + .frame(width:30,height:30) + .shadow(color: DamusColors.purple, radius: 2) + .opacity(isSideBarOpened ? 0 : 1) + .animation(isSideBarOpened ? .none : .default, value: isSideBarOpened) + .onTapGesture { + isSideBarOpened.toggle() + } + .padding(.leading) + + Spacer() + + HStack(alignment: .center) { + SignalView(state: damus_state, signal: home.signal) + } + } + .frame(maxWidth: .infinity, alignment: .trailing) + } + .padding(.horizontal, 20) + + VStack(spacing: 0) { + CustomPicker(tabs: [ + (NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts), + (NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies) + ], + selection: $filter_state) + + Divider() + .frame(height: 1) + } + } + .background { + DamusColors.adaptableWhite + .ignoresSafeArea() } } @@ -60,21 +118,26 @@ struct PostingTimelineView: View { PostButtonContainer(is_left_handed: damus_state.settings.left_handed) { self.active_sheet = .post(.posting(.none)) } + .padding(.bottom, tabHeight + getSafeAreaBottom()) + .opacity((abs(1.25 - (abs(headerOffset/100.0))))) } } } - .safeAreaInset(edge: .top, spacing: 0) { - VStack(spacing: 0) { - CustomPicker(tabs: [ - (NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts), - (NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies) - ], - selection: $filter_state) - - Divider() - .frame(height: 1) - } - .background(DamusColors.adaptableWhite) + .overlay(alignment: .top) { + HeaderView() + .anchorPreference(key: HeaderBoundsKey.self, value: .bounds){$0} + .overlayPreferenceValue(HeaderBoundsKey.self) { value in + GeometryReader{ proxy in + if let anchor = value{ + Color.clear + .onAppear { + headerHeight = proxy[anchor].height + } + } + } + } + .offset(y: -headerOffset < headerHeight ? headerOffset : (headerOffset < 0 ? headerOffset : 0)) + .opacity(1.0 - (abs(headerOffset/100.0))) } } } diff --git a/damus/Views/TimelineView.swift b/damus/Views/TimelineView.swift index b1466c51..8edff654 100644 --- a/damus/Views/TimelineView.swift +++ b/damus/Views/TimelineView.swift @@ -10,6 +10,11 @@ import SwiftUI struct TimelineView: View { @ObservedObject var events: EventHolder @Binding var loading: Bool + @Binding var headerHeight: CGFloat + @Binding var headerOffset: CGFloat + @State var shiftOffset: CGFloat = 0 + @State var lastHeaderOffset: CGFloat = 0 + @State var direction: SwipeDirection = .none let damus: DamusState let show_friend_icon: Bool @@ -17,9 +22,23 @@ struct TimelineView: View { let content: Content? let apply_mute_rules: Bool + init(events: EventHolder, loading: Binding, headerHeight: Binding, headerOffset: Binding, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) { + self.events = events + self._loading = loading + self._headerHeight = headerHeight + self._headerOffset = headerOffset + self.damus = damus + self.show_friend_icon = show_friend_icon + self.filter = filter + self.apply_mute_rules = apply_mute_rules + self.content = content?() + } + init(events: EventHolder, loading: Binding, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) { self.events = events self._loading = loading + self._headerHeight = .constant(0.0) + self._headerOffset = .constant(0.0) self.damus = damus self.show_friend_icon = show_friend_icon self.filter = filter @@ -38,20 +57,43 @@ struct TimelineView: View { content } - Color.white.opacity(0) + Color.clear .id("startblock") - .frame(height: 1) + .frame(height: 0) InnerTimelineView(events: events, damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules) .redacted(reason: loading ? .placeholder : []) .shimmer(loading) .disabled(loading) - .background(GeometryReader { proxy -> Color in - handle_scroll_queue(proxy, queue: self.events) - return Color.clear - }) + .padding(.top, headerHeight - getSafeAreaTop()) + .offsetY { previous, current in + if previous > current{ + if direction != .up && current < 0 { + shiftOffset = current - headerOffset + direction = .up + lastHeaderOffset = headerOffset + } + + let offset = current < 0 ? (current - shiftOffset) : 0 + headerOffset = (-offset < headerHeight ? (offset < 0 ? offset : 0) : -headerHeight) + }else { + if direction != .down { + shiftOffset = current + direction = .down + lastHeaderOffset = headerOffset + } + + let offset = lastHeaderOffset + (current - shiftOffset) + headerOffset = (offset > 0 ? 0 : offset) + } + } + .background { + GeometryReader { proxy -> Color in + handle_scroll_queue(proxy, queue: self.events) + return Color.clear + } + } } - //.buttonStyle(BorderlessButtonStyle()) .coordinateSpace(name: "scroll") .onReceive(handle_notify(.scroll_to_top)) { () in events.flush() diff --git a/damus/Views/Zaps/ZapsView.swift b/damus/Views/Zaps/ZapsView.swift index 52056a0a..de9a0097 100644 --- a/damus/Views/Zaps/ZapsView.swift +++ b/damus/Views/Zaps/ZapsView.swift @@ -28,6 +28,7 @@ struct ZapsView: View { } } } + .padding(.bottom, tabHeight + getSafeAreaBottom()) .navigationBarTitle(NSLocalizedString("Zaps", comment: "Navigation bar title for the Zaps view.")) .onAppear { model.subscribe()