commit b7053e8680e5bd69e9e9d6ba0b73bb79df842f03 parent 42f5af0ffd611b3181558d49bcffb3cdfb3ce3cd Author: William Casarin <jb55@jb55.com> Date: Tue, 8 Oct 2024 09:55:14 +0200 Merge 'ux: Seamless Timeline' ericholguin (1): ux: Seamless Timeline Diffstat:
20 files changed, 280 insertions(+), 62 deletions(-)
diff --git 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 */; }; @@ -398,6 +397,9 @@ 5C0567582C8FBC560073F23A /* NDBSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0567572C8FBC560073F23A /* NDBSearchView.swift */; }; 5C0567592C8FBDE30073F23A /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; 5C05675A2C8FBDE70073F23A /* NDBSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0567572C8FBC560073F23A /* NDBSearchView.swift */; }; + 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 */; }; @@ -1837,6 +1839,7 @@ 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVPlayer+Additions.swift"; sourceTree = "<group>"; }; 50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; }; 5C0567572C8FBC560073F23A /* NDBSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NDBSearchView.swift; sourceTree = "<group>"; }; + 5C0567542C8B60C20073F23A /* OffsetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsetExtension.swift; sourceTree = "<group>"; }; 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = "<group>"; }; 5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySoftwareDetail.swift; sourceTree = "<group>"; }; 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayAdminDetail.swift; sourceTree = "<group>"; }; @@ -3267,6 +3270,7 @@ 4C7D09752A0AF19E00943473 /* FillAndStroke.swift */, D72E12772BEED22400F4F781 /* Array.swift */, D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */, + 5C0567542C8B60C20073F23A /* OffsetExtension.swift */, ); path = Extensions; sourceTree = "<group>"; @@ -3806,6 +3810,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 */, @@ -4262,7 +4267,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 */, @@ -4454,6 +4458,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 */, @@ -4527,6 +4532,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 @@ -46,7 +46,6 @@ struct CustomPicker<SelectionValue: Hashable>: View { .accentColor(tag == selection ? textColor() : .gray) } } - .background(Color(UIColor.systemBackground)) } func textColor() -> Color { diff --git 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 @@ -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<CGRect>? + + static func reduce(value: inout Anchor<CGRect>?, nextValue: () -> Anchor<CGRect>?) { + 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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<AnyView>(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 @@ -10,6 +10,11 @@ import SwiftUI struct TimelineView<Content: View>: 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<Content: View>: View { let content: Content? let apply_mute_rules: Bool + init(events: EventHolder, loading: Binding<Bool>, headerHeight: Binding<CGFloat>, headerOffset: Binding<CGFloat>, 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<Bool>, 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<Content: View>: 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 @@ -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()