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 <ericholguin@apache.org>
This commit is contained in:
ericholguin
2024-09-06 09:12:16 -06:00
parent 51ee4046a0
commit 6751bc15cc
20 changed files with 280 additions and 62 deletions

View File

@@ -351,7 +351,6 @@
4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; }; 4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; };
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8795A2996C47A00F758CC /* ZapsModel.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"; }; }; 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 */; }; 4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; };
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */; }; 4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */; };
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.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 */; }; 50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
50C3E08A2AA8E3F7006A4BC0 /* AVPlayer+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.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 */; }; 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 */; }; 5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; };
5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */; }; 5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */; };
5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.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 = "<group>"; }; 50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; };
50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVPlayer+Additions.swift"; sourceTree = "<group>"; }; 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>"; }; 50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; 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>"; }; 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>"; }; 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>"; }; 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayAdminDetail.swift; sourceTree = "<group>"; };
@@ -3263,6 +3266,7 @@
4C7D09752A0AF19E00943473 /* FillAndStroke.swift */, 4C7D09752A0AF19E00943473 /* FillAndStroke.swift */,
D72E12772BEED22400F4F781 /* Array.swift */, D72E12772BEED22400F4F781 /* Array.swift */,
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */, D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */,
5C0567542C8B60C20073F23A /* OffsetExtension.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -3802,6 +3806,7 @@
50C3E08A2AA8E3F7006A4BC0 /* AVPlayer+Additions.swift in Sources */, 50C3E08A2AA8E3F7006A4BC0 /* AVPlayer+Additions.swift in Sources */,
4C198DF229F88C6B004C165C /* BlurHashDecode.swift in Sources */, 4C198DF229F88C6B004C165C /* BlurHashDecode.swift in Sources */,
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */, F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
5C0567552C8B60C20073F23A /* OffsetExtension.swift in Sources */,
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */, 4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */, 4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
4C32B94D2A9AD44700DC3548 /* Offset.swift in Sources */, 4C32B94D2A9AD44700DC3548 /* Offset.swift in Sources */,
@@ -4257,7 +4262,6 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
4CED18FD2C84B28F006AF665 /* PostingTimelineView.swift in Sources */,
D73E5E202C6A97F4007EB227 /* AttachedWalletNotify.swift in Sources */, D73E5E202C6A97F4007EB227 /* AttachedWalletNotify.swift in Sources */,
D73E5E212C6A97F4007EB227 /* DisplayTabBarNotify.swift in Sources */, D73E5E212C6A97F4007EB227 /* DisplayTabBarNotify.swift in Sources */,
D73E5E222C6A97F4007EB227 /* BroadcastNotify.swift in Sources */, D73E5E222C6A97F4007EB227 /* BroadcastNotify.swift in Sources */,
@@ -4448,6 +4452,7 @@
D73E5EDD2C6A97F4007EB227 /* NotificationSettingsView.swift in Sources */, D73E5EDD2C6A97F4007EB227 /* NotificationSettingsView.swift in Sources */,
D73E5EDE2C6A97F4007EB227 /* AppearanceSettingsView.swift in Sources */, D73E5EDE2C6A97F4007EB227 /* AppearanceSettingsView.swift in Sources */,
D73E5EDF2C6A97F4007EB227 /* KeySettingsView.swift in Sources */, D73E5EDF2C6A97F4007EB227 /* KeySettingsView.swift in Sources */,
5C0567562C8B60E60073F23A /* OffsetExtension.swift in Sources */,
D73E5EE02C6A97F4007EB227 /* ZapSettingsView.swift in Sources */, D73E5EE02C6A97F4007EB227 /* ZapSettingsView.swift in Sources */,
D73E5F792C6A9C4C007EB227 /* HomeModel.swift in Sources */, D73E5F792C6A9C4C007EB227 /* HomeModel.swift in Sources */,
D73E5EE12C6A97F4007EB227 /* TranslationSettingsView.swift in Sources */, D73E5EE12C6A97F4007EB227 /* TranslationSettingsView.swift in Sources */,
@@ -4521,6 +4526,7 @@
D73E5F242C6A97F4007EB227 /* HighlightLink.swift in Sources */, D73E5F242C6A97F4007EB227 /* HighlightLink.swift in Sources */,
D73E5F252C6A97F4007EB227 /* HighlightEventRef.swift in Sources */, D73E5F252C6A97F4007EB227 /* HighlightEventRef.swift in Sources */,
D73E5F262C6A97F4007EB227 /* HighlightDraftContentView.swift in Sources */, D73E5F262C6A97F4007EB227 /* HighlightDraftContentView.swift in Sources */,
5C0567532C8B5F9C0073F23A /* PostingTimelineView.swift in Sources */,
D73E5F272C6A97F4007EB227 /* TimeDot.swift in Sources */, D73E5F272C6A97F4007EB227 /* TimeDot.swift in Sources */,
D73E5F282C6A97F4007EB227 /* EventTop.swift in Sources */, D73E5F282C6A97F4007EB227 /* EventTop.swift in Sources */,
D73E5F292C6A97F4007EB227 /* ReplyDescription.swift in Sources */, D73E5F292C6A97F4007EB227 /* ReplyDescription.swift in Sources */,

View File

@@ -46,7 +46,6 @@ struct CustomPicker<SelectionValue: Hashable>: View {
.accentColor(tag == selection ? textColor() : .gray) .accentColor(tag == selection ? textColor() : .gray)
} }
} }
.background(Color(UIColor.systemBackground))
} }
func textColor() -> Color { func textColor() -> Color {

View File

@@ -61,6 +61,8 @@ func present_sheet(_ sheet: Sheets) {
notify(.present_sheet(sheet)) notify(.present_sheet(sheet))
} }
var tabHeight: CGFloat = 0.0
struct ContentView: View { struct ContentView: View {
let keypair: Keypair let keypair: Keypair
let appDelegate: AppDelegate? let appDelegate: AppDelegate?
@@ -89,6 +91,7 @@ struct ContentView: View {
@State var user_muted_confirm: Bool = false @State var user_muted_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false @State var confirm_overwrite_mutelist: Bool = false
@State private var isSideBarOpened = false @State private var isSideBarOpened = false
@State var headerOffset: CGFloat = 0.0
var home: HomeModel = HomeModel() var home: HomeModel = HomeModel()
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator() @StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
@AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false @AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false
@@ -131,7 +134,7 @@ struct ContentView: View {
} }
case .home: 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: case .notifications:
NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle) 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) 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) .navigationBarTitle(timeline_name(selected_timeline), displayMode: .inline)
.toolbar(selected_timeline != .home ? .visible : .hidden)
.toolbar { .toolbar {
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
VStack { VStack {
if selected_timeline == .home { timelineNavItem
Image("damus-home") .opacity(isSideBarOpened ? 0 : 1)
.resizable() .animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
.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)
}
} }
} }
} }
@@ -237,9 +231,11 @@ struct ContentView: View {
} }
} }
} }
.background(DamusColors.adaptableWhite)
.edgesIgnoringSafeArea(selected_timeline != .home ? [] : [.top, .bottom])
.tabViewStyle(.page(indexDisplayMode: .never)) .tabViewStyle(.page(indexDisplayMode: .never))
.overlay( .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 .navigationDestination(for: Route.self) { route in
route.view(navigationCoordinator: navigationCoordinator, damusState: damus_state!) route.view(navigationCoordinator: navigationCoordinator, damusState: damus_state!)
@@ -249,13 +245,25 @@ struct ContentView: View {
} }
} }
.navigationViewStyle(.stack) .navigationViewStyle(.stack)
.overlay(alignment: .bottom) {
if !hide_bar { if !hide_bar {
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline) if !isSideBarOpened {
.padding([.bottom], 8) TabBar(nstatus: home.notification_status, navIsAtRoot: navIsAtRoot(), selected: $selected_timeline, headerOffset: $headerOffset, settings: damus.settings, action: switch_timeline)
.background(Color(uiColor: .systemBackground).ignoresSafeArea()) .padding([.bottom], 8)
} else { .background(selected_timeline != .home || (selected_timeline == .home && !self.navIsAtRoot()) ? DamusColors.adaptableWhite : DamusColors.adaptableWhite.opacity(abs(1.25 - (abs(headerOffset/100.0)))))
Text("") .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
}
}
}
}
}
}
} }
} }
} }

View File

@@ -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
}

View File

@@ -39,6 +39,7 @@ struct BookmarksView: View {
ScrollView { ScrollView {
InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, filter: noneFilter) InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, filter: noneFilter)
} }
.padding(.bottom, 10 + tabHeight + getSafeAreaBottom())
} }
} }
.onReceive(handle_notify(.switched_timeline)) { _ in .onReceive(handle_notify(.switched_timeline)) { _ in

View File

@@ -135,6 +135,9 @@ struct ChatroomThreadView: View {
} }
.padding(.top) .padding(.top)
EndBlock() EndBlock()
HStack {}
.frame(height: tabHeight + getSafeAreaBottom())
} }
.onReceive(handle_notify(.post), perform: { notify in .onReceive(handle_notify(.post), perform: { notify in
switch notify { switch notify {

View File

@@ -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) Text(verbatim: VersionInfo.version)
.contextMenu { .contextMenu {
Button { Button {

View File

@@ -66,7 +66,9 @@ struct TabButton: View {
struct TabBar: View { struct TabBar: View {
var nstatus: NotificationStatusModel var nstatus: NotificationStatusModel
var navIsAtRoot: Bool
@Binding var selected: Timeline @Binding var selected: Timeline
@Binding var headerOffset: CGFloat
let settings: UserSettingsStore let settings: UserSettingsStore
let action: (Timeline) -> () 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") 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)))))
} }
} }

View File

@@ -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 ForEach(threads, id: \.self) { item in
if case let MuteItem.thread(note_id, _) = item { if case let MuteItem.thread(note_id, _) = item {
if let event = damus_state.events.lookup(note_id) { if let event = damus_state.events.lookup(note_id) {

View File

@@ -203,7 +203,7 @@ struct EditMetadataView: View {
}) })
.buttonStyle(GradientButtonStyle(padding: 15)) .buttonStyle(GradientButtonStyle(padding: 15))
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.bottom, 10) .padding(.bottom, 10 + tabHeight + getSafeAreaBottom())
.disabled(!didChange()) .disabled(!didChange())
.opacity(!didChange() ? 0.5 : 1) .opacity(!didChange() ? 0.5 : 1)
.disabled(profileUploadObserver.isLoading || bannerUploadObserver.isLoading) .disabled(profileUploadObserver.isLoading || bannerUploadObserver.isLoading)

View File

@@ -444,6 +444,7 @@ struct ProfileView: View {
.zIndex(-yOffset > navbarHeight ? 0 : 1) .zIndex(-yOffset > navbarHeight ? 0 : 1)
} }
} }
.padding(.bottom, tabHeight + getSafeAreaBottom())
.ignoresSafeArea() .ignoresSafeArea()
.navigationTitle("") .navigationTitle("")
.navigationBarBackButtonHidden() .navigationBarBackButtonHidden()
@@ -485,6 +486,7 @@ struct ProfileView: View {
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)
} }
} }
} }

View File

@@ -22,6 +22,7 @@ struct ReactionsView: View {
} }
.padding() .padding()
} }
.padding(.bottom, tabHeight + getSafeAreaBottom())
.navigationBarTitle(NSLocalizedString("Reactions", comment: "Navigation bar title for Reactions view.")) .navigationBarTitle(NSLocalizedString("Reactions", comment: "Navigation bar title for Reactions view."))
.onAppear { .onAppear {
model.subscribe() model.subscribe()

View File

@@ -13,15 +13,14 @@ struct SignalView: View {
var body: some View { var body: some View {
Group { Group {
if signal.signal != signal.max_signal { NavigationLink(value: Route.RelayConfig) {
NavigationLink(value: Route.RelayConfig) { Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.") .font(.callout)
.font(.callout) .foregroundColor(.gray)
.foregroundColor(.gray)
}
} else {
Text("")
} }
.frame(width:50,height:30)
.opacity(signal.signal != signal.max_signal ? 1 : 0)
.disabled(signal.signal == signal.max_signal)
} }
} }

View File

@@ -20,6 +20,7 @@ struct RepostsView: View {
} }
.padding() .padding()
} }
.padding(.bottom, tabHeight + getSafeAreaBottom())
.navigationBarTitle(NSLocalizedString("Reposts", comment: "Navigation bar title for Reposts view.")) .navigationBarTitle(NSLocalizedString("Reposts", comment: "Navigation bar title for Reposts view."))
.onAppear { .onAppear {
model.subscribe() model.subscribe()

View File

@@ -108,6 +108,7 @@ struct AppearanceSettingsView: View {
Section( Section(
header: Text("Profiles", comment: "Section title for profile view configuration."), 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") 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) 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) .toggleStyle(.switch)

View File

@@ -177,7 +177,10 @@ struct NotificationSettingsView: View {
.toggleStyle(.switch) .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)) Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: indicator_binding(.zaps))
.toggleStyle(.switch) .toggleStyle(.switch)
Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: indicator_binding(.mentions)) Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: indicator_binding(.mentions))

View File

@@ -11,6 +11,7 @@ import SwiftUI
struct SideMenuView: View { struct SideMenuView: View {
let damus_state: DamusState let damus_state: DamusState
@Binding var isSidebarVisible: Bool @Binding var isSidebarVisible: Bool
@Binding var selected: Timeline
@State var confirm_logout: Bool = false @State var confirm_logout: Bool = false
@State private var showQRCode = false @State private var showQRCode = false
@@ -200,7 +201,7 @@ struct SideMenuView: View {
} }
.padding(.top, verticalSpacing) .padding(.top, verticalSpacing)
} }
.padding(.top, -(padding / 2.0)) .padding(.top, selected != .home ? -(padding / 2.0) : 30)
.padding([.leading, .trailing, .bottom], padding) .padding([.leading, .trailing, .bottom], padding)
} }
.frame(width: sideBarWidth) .frame(width: sideBarWidth)
@@ -249,6 +250,6 @@ struct SideMenuView: View {
struct Previews_SideMenuView_Previews: PreviewProvider { struct Previews_SideMenuView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let ds = test_damus_state let ds = test_damus_state
SideMenuView(damus_state: ds, isSidebarVisible: .constant(true)) SideMenuView(damus_state: ds, isSidebarVisible: .constant(true), selected: .constant(.home))
} }
} }

View File

@@ -16,11 +16,14 @@ struct PostingTimelineView: View {
@State var initialOffset: CGFloat? @State var initialOffset: CGFloat?
@State var offset: CGFloat? @State var offset: CGFloat?
@State var showSearch: Bool = true @State var showSearch: Bool = true
@Binding var isSideBarOpened: Bool
@Binding var active_sheet: Sheets? @Binding var active_sheet: Sheets?
@FocusState private var isSearchFocused: Bool @FocusState private var isSearchFocused: Bool
@State private var contentOffset: CGFloat = 0 @State private var contentOffset: CGFloat = 0
@State private var indicatorWidth: CGFloat = 0 @State private var indicatorWidth: CGFloat = 0
@State private var indicatorPosition: 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 @SceneStorage("PostingTimelineView.filter_state") var filter_state : FilterState = .posts_and_replies
var mystery: some View { var mystery: some View {
@@ -35,8 +38,63 @@ struct PostingTimelineView: View {
} }
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some 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) { TimelineView<AnyView>(events: home.events, loading: .constant(false), headerHeight: $headerHeight, headerOffset: $headerOffset, damus: damus_state, show_friend_icon: false, filter: filter)
PullDownSearchView(state: damus_state, on_cancel: {}) }
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) { PostButtonContainer(is_left_handed: damus_state.settings.left_handed) {
self.active_sheet = .post(.posting(.none)) self.active_sheet = .post(.posting(.none))
} }
.padding(.bottom, tabHeight + getSafeAreaBottom())
.opacity((abs(1.25 - (abs(headerOffset/100.0)))))
} }
} }
} }
.safeAreaInset(edge: .top, spacing: 0) { .overlay(alignment: .top) {
VStack(spacing: 0) { HeaderView()
CustomPicker(tabs: [ .anchorPreference(key: HeaderBoundsKey.self, value: .bounds){$0}
(NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts), .overlayPreferenceValue(HeaderBoundsKey.self) { value in
(NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies) GeometryReader{ proxy in
], if let anchor = value{
selection: $filter_state) Color.clear
.onAppear {
Divider() headerHeight = proxy[anchor].height
.frame(height: 1) }
} }
.background(DamusColors.adaptableWhite) }
}
.offset(y: -headerOffset < headerHeight ? headerOffset : (headerOffset < 0 ? headerOffset : 0))
.opacity(1.0 - (abs(headerOffset/100.0)))
} }
} }
} }

View File

@@ -10,6 +10,11 @@ import SwiftUI
struct TimelineView<Content: View>: View { struct TimelineView<Content: View>: View {
@ObservedObject var events: EventHolder @ObservedObject var events: EventHolder
@Binding var loading: Bool @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 damus: DamusState
let show_friend_icon: Bool let show_friend_icon: Bool
@@ -17,9 +22,23 @@ struct TimelineView<Content: View>: View {
let content: Content? let content: Content?
let apply_mute_rules: Bool 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) { 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.events = events
self._loading = loading self._loading = loading
self._headerHeight = .constant(0.0)
self._headerOffset = .constant(0.0)
self.damus = damus self.damus = damus
self.show_friend_icon = show_friend_icon self.show_friend_icon = show_friend_icon
self.filter = filter self.filter = filter
@@ -38,20 +57,43 @@ struct TimelineView<Content: View>: View {
content content
} }
Color.white.opacity(0) Color.clear
.id("startblock") .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) InnerTimelineView(events: events, damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules)
.redacted(reason: loading ? .placeholder : []) .redacted(reason: loading ? .placeholder : [])
.shimmer(loading) .shimmer(loading)
.disabled(loading) .disabled(loading)
.background(GeometryReader { proxy -> Color in .padding(.top, headerHeight - getSafeAreaTop())
handle_scroll_queue(proxy, queue: self.events) .offsetY { previous, current in
return Color.clear 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") .coordinateSpace(name: "scroll")
.onReceive(handle_notify(.scroll_to_top)) { () in .onReceive(handle_notify(.scroll_to_top)) { () in
events.flush() events.flush()

View File

@@ -28,6 +28,7 @@ struct ZapsView: View {
} }
} }
} }
.padding(.bottom, tabHeight + getSafeAreaBottom())
.navigationBarTitle(NSLocalizedString("Zaps", comment: "Navigation bar title for the Zaps view.")) .navigationBarTitle(NSLocalizedString("Zaps", comment: "Navigation bar title for the Zaps view."))
.onAppear { .onAppear {
model.subscribe() model.subscribe()