diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index fc677b4e..da7bce4f 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -36,7 +36,7 @@ 9F2A542A296AF557009B2D7C /* NotificationServiceSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2A5429296AF557009B2D7C /* NotificationServiceSupport.swift */; }; 9F2A542C296B1177009B2D7C /* glass.caf in Resources */ = {isa = PBXBuildFile; fileRef = 9F2A542B296B1177009B2D7C /* glass.caf */; }; 9F2A542E296B1CC0009B2D7C /* glass.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9F2A542D296B1CC0009B2D7C /* glass.wav */; }; - 9F2B92F6295AE04800DE16D0 /* AppTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F5295AE04800DE16D0 /* AppTab.swift */; }; + 9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F5295AE04800DE16D0 /* Tabs.swift */; }; 9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */; }; 9F2B92FC295DA94500DE16D0 /* InstanceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */; }; 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4629506F6600B3281A /* NotificationTab.swift */; }; @@ -102,12 +102,14 @@ 9FAD85A0297456A100496AB1 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAD859F297456A100496AB1 /* Models */; }; 9FAD85A2297456A400496AB1 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAD85A1297456A400496AB1 /* Env */; }; 9FAD85A4297456A800496AB1 /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAD85A3297456A800496AB1 /* DesignSystem */; }; + 9FAD85CF2975B68900496AB1 /* SideBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD85CE2975B68900496AB1 /* SideBarView.swift */; }; 9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAE4ACA293783B000772766 /* SettingsTab.swift */; }; 9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAE4ACD29379A5A00772766 /* KeychainSwift */; }; 9FB143D12983104700A27BB1 /* glass.caf in Resources */ = {isa = PBXBuildFile; fileRef = 9F2A542B296B1177009B2D7C /* glass.caf */; }; 9FB143D22983104A00A27BB1 /* glass.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9F2A542D296B1CC0009B2D7C /* glass.wav */; }; 9FB183222AE9268800BBB692 /* IceCubesApp+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB183212AE9268800BBB692 /* IceCubesApp+Menu.swift */; }; 9FB183292AE9449100BBB692 /* IceCubesApp+Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB183282AE9449100BBB692 /* IceCubesApp+Scene.swift */; }; + 9FBA1D352B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA1D342B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift */; }; 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */; }; 9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; }; 9FC14EF22B494D180006CEE1 /* TagsGroupSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC14EF12B494D180006CEE1 /* TagsGroupSettingView.swift */; }; @@ -229,7 +231,7 @@ 9F2A5429296AF557009B2D7C /* NotificationServiceSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceSupport.swift; sourceTree = ""; }; 9F2A542B296B1177009B2D7C /* glass.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = glass.caf; sourceTree = ""; }; 9F2A542D296B1CC0009B2D7C /* glass.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = glass.wav; sourceTree = ""; }; - 9F2B92F5295AE04800DE16D0 /* AppTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTab.swift; sourceTree = ""; }; + 9F2B92F5295AE04800DE16D0 /* Tabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tabs.swift; sourceTree = ""; }; 9F2B92F9295DA7D700DE16D0 /* AddAccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountsView.swift; sourceTree = ""; }; 9F2B92FB295DA94500DE16D0 /* InstanceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceInfoView.swift; sourceTree = ""; }; 9F35DB45294FA04C00B3281A /* DesignSystem */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DesignSystem; path = Packages/DesignSystem; sourceTree = ""; }; @@ -284,10 +286,12 @@ 9FAD858A29743F7400496AB1 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; 9FAD858F29743F7400496AB1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9FAD859629743F7E00496AB1 /* IceCubesShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesShareExtension.entitlements; sourceTree = ""; }; + 9FAD85CE2975B68900496AB1 /* SideBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideBarView.swift; sourceTree = ""; }; 9FAE4AC8293774FF00772766 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 9FAE4ACA293783B000772766 /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = ""; }; 9FB183212AE9268800BBB692 /* IceCubesApp+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceCubesApp+Menu.swift"; sourceTree = ""; }; 9FB183282AE9449100BBB692 /* IceCubesApp+Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceCubesApp+Scene.swift"; sourceTree = ""; }; + 9FBA1D342B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarEntriesSettingsView.swift; sourceTree = ""; }; 9FBFE639292A715500C250E9 /* Ice Cubes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ice Cubes.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesApp.swift; sourceTree = ""; }; 9FC14EF12B494D180006CEE1 /* TagsGroupSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsGroupSettingView.swift; sourceTree = ""; }; @@ -456,6 +460,7 @@ 9FAE4AC9293783A200772766 /* Tabs */, 9F398AA52935FE8A00A889F2 /* AppRegistry.swift */, 639CDF9B296AC82F00C35E58 /* SafariRouter.swift */, + 9FAD85CE2975B68900496AB1 /* SideBarView.swift */, ); path = App; sourceTree = ""; @@ -560,7 +565,7 @@ 9F35DB4629506F6600B3281A /* NotificationTab.swift */, 9F35DB4B2952005C00B3281A /* MessagesTab.swift */, 9F55C68C2955968700F94077 /* ExploreTab.swift */, - 9F2B92F5295AE04800DE16D0 /* AppTab.swift */, + 9F2B92F5295AE04800DE16D0 /* Tabs.swift */, 9F4A48182976B21900A1A038 /* ProfileTab.swift */, 9F15D5FF2B3D6A850008C220 /* NavigationTab.swift */, 9F15D6032B3DC2180008C220 /* NavigationSheet.swift */, @@ -667,6 +672,7 @@ 9FC14EF12B494D180006CEE1 /* TagsGroupSettingView.swift */, 9FC14EF32B494D940006CEE1 /* RemoteTimelinesSettingView.swift */, 9FC14EF52B494DFF0006CEE1 /* RecenTagsSettingView.swift */, + 9FBA1D342B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift */, ); path = Settings; sourceTree = ""; @@ -1065,6 +1071,7 @@ 9F37BDE12BE38646007F28AD /* PostImageIntent.swift in Sources */, 9F37BDDF2BE37C35007F28AD /* TabIntent.swift in Sources */, 9F7788C02BE63935004E6BEF /* InlinePostIntent.swift in Sources */, + 9FAD85CF2975B68900496AB1 /* SideBarView.swift in Sources */, 9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */, 9FC14EF42B494D940006CEE1 /* RemoteTimelinesSettingView.swift in Sources */, 9FC14EF22B494D180006CEE1 /* TagsGroupSettingView.swift in Sources */, @@ -1072,7 +1079,7 @@ 9F7335F92968576500AFF0BA /* DisplaySettingsView.swift in Sources */, 9F2A540729699698009B2D7C /* SupportAppView.swift in Sources */, 9FB183292AE9449100BBB692 /* IceCubesApp+Scene.swift in Sources */, - 9F2B92F6295AE04800DE16D0 /* AppTab.swift in Sources */, + 9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */, FA31A9AB2A66BF7C00D5F662 /* EditTagGroupView.swift in Sources */, 9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */, 9F398AA62935FE8A00A889F2 /* AppRegistry.swift in Sources */, @@ -1092,6 +1099,7 @@ 9F6028562B3F36AE00476078 /* AppView.swift in Sources */, 9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */, 9F1E8B47298EBCBB00609F80 /* HapticSettingsView.swift in Sources */, + 9FBA1D352B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift in Sources */, 9F2A5411296A1429009B2D7C /* PushNotificationsView.swift in Sources */, 9F6028582B3F3B7600476078 /* ToolbarTab.swift in Sources */, ); @@ -1179,7 +1187,7 @@ INFOPLIST_FILE = IceCubesNotifications/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1214,7 +1222,7 @@ INFOPLIST_FILE = IceCubesNotifications/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1253,7 +1261,7 @@ INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1289,7 +1297,7 @@ INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1322,7 +1330,7 @@ INFOPLIST_FILE = IceCubesShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1356,7 +1364,7 @@ INFOPLIST_FILE = IceCubesShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1540,7 +1548,7 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; @@ -1565,7 +1573,7 @@ SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; - _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = NO; + _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = YES; }; name = Debug; }; @@ -1607,7 +1615,7 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; @@ -1632,7 +1640,7 @@ SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; - _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = NO; + _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = YES; }; name = Release; }; @@ -1651,7 +1659,7 @@ INFOPLIST_FILE = IceCubesActionExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1686,7 +1694,7 @@ INFOPLIST_FILE = IceCubesActionExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/IceCubesApp/App/AppRegistry.swift b/IceCubesApp/App/AppRegistry.swift index 398d35a7..9a6eb754 100644 --- a/IceCubesApp/App/AppRegistry.swift +++ b/IceCubesApp/App/AppRegistry.swift @@ -133,7 +133,7 @@ extension View { StatusEditHistoryView(statusId: status) .withEnvironments() case .settings: - SettingsTabs(isModal: true) + SettingsTabs(popToRootTab: .constant(.settings), isModal: true) .withEnvironments() .preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light) case .accountPushNotficationsSettings: diff --git a/IceCubesApp/App/Main/AppView.swift b/IceCubesApp/App/Main/AppView.swift index dc1dc12d..ff72955c 100644 --- a/IceCubesApp/App/Main/AppView.swift +++ b/IceCubesApp/App/Main/AppView.swift @@ -21,12 +21,12 @@ struct AppView: View { @Environment(\.openWindow) var openWindow @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @Binding var selectedTab: AppTab + @Binding var selectedTab: Tab @Binding var appRouterPath: RouterPath + @State var popToRootTab: Tab = .other @State var iosTabs = iOSTabs.shared - @AppStorage("SidebarTabsCustomization") - private var sidebarTabsCustomization: TabViewCustomization + @State var sidebarTabs = SidebarTabs.shared var body: some View { #if os(visionOS) @@ -40,16 +40,16 @@ struct AppView: View { #endif } - var availableTabs: [AppTab] { + var availableTabs: [Tab] { guard appAccountsManager.currentClient.isAuth else { - return AppTab.loggedOutTab() + return Tab.loggedOutTab() } if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact { return iosTabs.tabs } else if UIDevice.current.userInterfaceIdiom == .vision { - return AppTab.visionOSTab() + return Tab.visionOSTab() } - return AppTab.sideBarTab() + return sidebarTabs.tabs.map { $0.tab } } var tabBarView: some View { @@ -64,31 +64,39 @@ struct AppView: View { #endif return } + if newTab == selectedTab { + /// Stupid hack to trigger onChange binding in tab views. + popToRootTab = .other + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + popToRootTab = selectedTab + } + } + HapticManager.shared.fireHaptic(.tabSelection) SoundEffectManager.shared.playSound(.tabSelection) selectedTab = newTab })) { ForEach(availableTabs) { tab in - Tab(value: tab) { - tab.makeContentView(selectedTab: $selectedTab) - .toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .tabBar) - } label: { - if userPreferences.showiPhoneTabLabel { - tab.label - .environment(\.symbolVariants, tab == selectedTab ? .fill : .none) - } else { - Image(systemName: tab.iconName) + tab.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab) + .tabItem { + if userPreferences.showiPhoneTabLabel { + tab.label + .environment(\.symbolVariants, tab == selectedTab ? .fill : .none) + } else { + Image(systemName: tab.iconName) + } } - } - .badge(badgeFor(tab: tab)) + .tag(tab) + .badge(badgeFor(tab: tab)) + .toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .tabBar) } } .id(appAccountsManager.currentClient.id) .withSheetDestinations(sheetDestinations: $appRouterPath.presentedSheet) } - private func badgeFor(tab: AppTab) -> Int { + private func badgeFor(tab: Tab) -> Int { if tab == .notifications, selectedTab != tab, let token = appAccountsManager.currentAccount.oauthToken { @@ -99,38 +107,42 @@ struct AppView: View { #if !os(visionOS) var sidebarView: some View { - HStack(spacing: 0) { - TabView(selection: $selectedTab) { - ForEach(availableTabs) { tab in - Tab(value: tab, role: tab == .explore ? .search : nil) { + SideBarView(selectedTab: $selectedTab, + popToRootTab: $popToRootTab, + tabs: availableTabs) + { + HStack(spacing: 0) { + TabView(selection: $selectedTab) { + ForEach(availableTabs) { tab in tab - .makeContentView(selectedTab: $selectedTab) - } label: { - tab.label + .makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab) + .tabItem { + tab.label + } + .tag(tab) } - .customizationID(tab.iconName) - .badge(badgeFor(tab: tab)) + } + .introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in + tabview.tabBar.isHidden = horizontalSizeClass == .regular + tabview.customizableViewControllers = [] + tabview.moreNavigationController.isNavigationBarHidden = true + } + if horizontalSizeClass == .regular, + appAccountsManager.currentClient.isAuth, + userPreferences.showiPadSecondaryColumn + { + Divider().edgesIgnoringSafeArea(.all) + notificationsSecondaryColumn } } - .tabViewStyle(.sidebarAdaptable) - .tabViewCustomization($sidebarTabsCustomization) - .introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in - tabview.customizableViewControllers = [] - tabview.moreNavigationController.isNavigationBarHidden = true - } - if horizontalSizeClass == .regular, - appAccountsManager.currentClient.isAuth, - userPreferences.showiPadSecondaryColumn - { - Divider().edgesIgnoringSafeArea(.all) - notificationsSecondaryColumn - } } + .environment(appRouterPath) } #endif var notificationsSecondaryColumn: some View { - NotificationsTab(selectedTab: .constant(.notifications), lockedType: nil) + NotificationsTab(selectedTab: .constant(.notifications), + popToRootTab: $popToRootTab, lockedType: nil) .environment(\.isSecondaryColumn, true) .frame(maxWidth: .secondaryColumnWidth) .id(appAccountsManager.currentAccount.id) diff --git a/IceCubesApp/App/Main/IceCubesApp.swift b/IceCubesApp/App/Main/IceCubesApp.swift index 70e1aa75..cc64b64d 100644 --- a/IceCubesApp/App/Main/IceCubesApp.swift +++ b/IceCubesApp/App/Main/IceCubesApp.swift @@ -28,7 +28,7 @@ struct IceCubesApp: App { @State var quickLook = QuickLook.shared @State var theme = Theme.shared - @State var selectedTab: AppTab = .timeline + @State var selectedTab: Tab = .timeline @State var appRouterPath = RouterPath() @State var isSupporter: Bool = false diff --git a/IceCubesApp/App/SideBarView.swift b/IceCubesApp/App/SideBarView.swift new file mode 100644 index 00000000..d5f5b69b --- /dev/null +++ b/IceCubesApp/App/SideBarView.swift @@ -0,0 +1,245 @@ +import Account +import AppAccount +import DesignSystem +import Env +import Models +import SwiftUI +import SwiftUIIntrospect + +@MainActor +struct SideBarView: View { + @Environment(\.openWindow) private var openWindow + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + @Environment(AppAccountsManager.self) private var appAccounts + @Environment(CurrentAccount.self) private var currentAccount + @Environment(Theme.self) private var theme + @Environment(StreamWatcher.self) private var watcher + @Environment(UserPreferences.self) private var userPreferences + @Environment(RouterPath.self) private var routerPath + + @Binding var selectedTab: Tab + @Binding var popToRootTab: Tab + var tabs: [Tab] + @ViewBuilder var content: () -> Content + + @State private var sidebarTabs = SidebarTabs.shared + + private func badgeFor(tab: Tab) -> Int { + if tab == .notifications, selectedTab != tab, + let token = appAccounts.currentAccount.oauthToken + { + return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0) + } + return 0 + } + + private func makeIconForTab(tab: Tab) -> some View { + HStack { + ZStack(alignment: .topTrailing) { + SideBarIcon(systemIconName: tab.iconName, + isSelected: tab == selectedTab) + let badge = badgeFor(tab: tab) + if badge > 0 { + makeBadgeView(count: badge) + } + } + if userPreferences.isSidebarExpanded { + Text(tab.title) + .font(.headline) + .foregroundColor(tab == selectedTab ? theme.tintColor : theme.labelColor) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .frame(width: (userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - 24, height: 50) + .background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear, + in: RoundedRectangle(cornerRadius: 8)) + } + + private func makeBadgeView(count: Int) -> some View { + ZStack { + Circle() + .fill(.red) + Text(count > 99 ? "99+" : String(count)) + .foregroundColor(.white) + .font(.caption2) + } + .frame(width: 24, height: 24) + .offset(x: 14, y: -14) + } + + private var postButton: some View { + Button { + #if targetEnvironment(macCatalyst) || os(visionOS) + openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)) + #else + routerPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) + #endif + } label: { + Image(systemName: "square.and.pencil") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 30) + .offset(x: 2, y: -2) + } + .buttonStyle(.borderedProminent) + .help(Tab.post.title) + } + + private func makeAccountButton(account: AppAccount, showBadge: Bool) -> some View { + Button { + if account.id == appAccounts.currentAccount.id { + selectedTab = .profile + SoundEffectManager.shared.playSound(.tabSelection) + } else { + var transation = Transaction() + transation.disablesAnimations = true + withTransaction(transation) { + appAccounts.currentAccount = account + } + } + } label: { + ZStack(alignment: .topTrailing) { + if userPreferences.isSidebarExpanded { + AppAccountView(viewModel: .init(appAccount: account, + isCompact: false, + isInSettings: false), + isParentPresented: .constant(false)) + } else { + AppAccountView(viewModel: .init(appAccount: account, + isCompact: true, + isInSettings: false), + isParentPresented: .constant(false)) + } + if !userPreferences.isSidebarExpanded, + showBadge, + let token = account.oauthToken, + let notificationsCount = userPreferences.notificationsCount[token], + notificationsCount > 0 + { + makeBadgeView(count: notificationsCount) + } + } + .padding(.leading, userPreferences.isSidebarExpanded ? 16 : 0) + } + .help(accountButtonTitle(accountName: account.accountName)) + .frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth, height: 50) + .padding(.vertical, 8) + .background(selectedTab == .profile && account.id == appAccounts.currentAccount.id ? + theme.secondaryBackgroundColor : .clear) + } + + private func accountButtonTitle(accountName: String?) -> LocalizedStringKey { + if let accountName { + "tab.profile-account-\(accountName)" + } else { + Tab.profile.title + } + } + + private var tabsView: some View { + ForEach(tabs) { tab in + if tab != .profile && sidebarTabs.isEnabled(tab) { + Button { + // ensure keyboard is always dismissed when selecting a tab + hideKeyboard() + + if tab == selectedTab { + popToRootTab = .other + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + popToRootTab = tab + } + } + selectedTab = tab + SoundEffectManager.shared.playSound(.tabSelection) + if tab == .notifications { + if let token = appAccounts.currentAccount.oauthToken { + userPreferences.notificationsCount[token] = 0 + } + watcher.unreadNotificationsCount = 0 + } + } label: { + makeIconForTab(tab: tab) + } + .help(tab.title) + } + } + } + + var body: some View { + @Bindable var routerPath = routerPath + HStack(spacing: 0) { + if horizontalSizeClass == .regular { + ScrollView { + VStack(alignment: .center) { + if appAccounts.availableAccounts.isEmpty { + tabsView + } else { + ForEach(appAccounts.availableAccounts) { account in + makeAccountButton(account: account, + showBadge: account.id != appAccounts.currentAccount.id) + if account.id == appAccounts.currentAccount.id { + tabsView + } + } + } + } + } + .frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) + .scrollContentBackground(.hidden) + .background(.thinMaterial) + .safeAreaInset(edge: .bottom, content: { + HStack(spacing: 16) { + postButton + .padding(.vertical, 24) + .padding(.leading, userPreferences.isSidebarExpanded ? 18 : 0) + if userPreferences.isSidebarExpanded { + Text("menu.new-post") + .font(.subheadline) + .foregroundColor(theme.labelColor) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) + .background(.thinMaterial) + }) + Divider().edgesIgnoringSafeArea(.all) + } + content() + } + .background(.thinMaterial) + .edgesIgnoringSafeArea(.bottom) + .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) + } +} + +private struct SideBarIcon: View { + @Environment(Theme.self) private var theme + + let systemIconName: String + let isSelected: Bool + + @State private var isHovered: Bool = false + + var body: some View { + Image(systemName: systemIconName) + .font(.title2) + .fontWeight(.medium) + .foregroundColor(isSelected ? theme.tintColor : theme.labelColor) + .symbolVariant(isSelected ? .fill : .none) + .scaleEffect(isHovered ? 0.8 : 1.0) + .onHover { isHovered in + withAnimation(.interpolatingSpring(stiffness: 300, damping: 15)) { + self.isHovered = isHovered + } + } + .frame(width: 50, height: 40) + } +} + +extension View { + @MainActor func hideKeyboard() { + let resign = #selector(UIResponder.resignFirstResponder) + UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil) + } +} diff --git a/IceCubesApp/App/Tabs/ExploreTab.swift b/IceCubesApp/App/Tabs/ExploreTab.swift index e36234d5..5b2cce1c 100644 --- a/IceCubesApp/App/Tabs/ExploreTab.swift +++ b/IceCubesApp/App/Tabs/ExploreTab.swift @@ -14,6 +14,7 @@ struct ExploreTab: View { @Environment(Client.self) private var client @State private var routerPath = RouterPath() @State private var scrollToTopSignal: Int = 0 + @Binding var popToRootTab: Tab var body: some View { NavigationStack(path: $routerPath.path) { @@ -27,6 +28,15 @@ struct ExploreTab: View { } .withSafariRouter() .environment(routerPath) + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .explore { + if routerPath.path.isEmpty { + scrollToTopSignal += 1 + } else { + routerPath.path = [] + } + } + } .onChange(of: client.id) { routerPath.path = [] } diff --git a/IceCubesApp/App/Tabs/MessagesTab.swift b/IceCubesApp/App/Tabs/MessagesTab.swift index 76e73e61..d265b73d 100644 --- a/IceCubesApp/App/Tabs/MessagesTab.swift +++ b/IceCubesApp/App/Tabs/MessagesTab.swift @@ -16,6 +16,7 @@ struct MessagesTab: View { @Environment(AppAccountsManager.self) private var appAccount @State private var routerPath = RouterPath() @State private var scrollToTopSignal: Int = 0 + @Binding var popToRootTab: Tab var body: some View { NavigationStack(path: $routerPath.path) { @@ -28,6 +29,15 @@ struct MessagesTab: View { .toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar) .id(client.id) } + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .messages { + if routerPath.path.isEmpty { + scrollToTopSignal += 1 + } else { + routerPath.path = [] + } + } + } .onChange(of: client.id) { routerPath.path = [] } diff --git a/IceCubesApp/App/Tabs/NotificationTab.swift b/IceCubesApp/App/Tabs/NotificationTab.swift index 2ab6f805..60713ee1 100644 --- a/IceCubesApp/App/Tabs/NotificationTab.swift +++ b/IceCubesApp/App/Tabs/NotificationTab.swift @@ -22,7 +22,8 @@ struct NotificationsTab: View { @State private var routerPath = RouterPath() @State private var scrollToTopSignal: Int = 0 - @Binding var selectedTab: AppTab + @Binding var selectedTab: Tab + @Binding var popToRootTab: Tab let lockedType: Models.Notification.NotificationType? @@ -50,6 +51,15 @@ struct NotificationsTab: View { } .withSafariRouter() .environment(routerPath) + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .notifications { + if routerPath.path.isEmpty { + scrollToTopSignal += 1 + } else { + routerPath.path = [] + } + } + } .onChange(of: selectedTab) { _, _ in clearNotifications() } diff --git a/IceCubesApp/App/Tabs/ProfileTab.swift b/IceCubesApp/App/Tabs/ProfileTab.swift index 12670823..22a80747 100644 --- a/IceCubesApp/App/Tabs/ProfileTab.swift +++ b/IceCubesApp/App/Tabs/ProfileTab.swift @@ -15,6 +15,7 @@ struct ProfileTab: View { @Environment(CurrentAccount.self) private var currentAccount @State private var routerPath = RouterPath() @State private var scrollToTopSignal: Int = 0 + @Binding var popToRootTab: Tab var body: some View { NavigationStack(path: $routerPath.path) { @@ -30,6 +31,15 @@ struct ProfileTab: View { .allowsHitTesting(false) } } + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .profile { + if routerPath.path.isEmpty { + scrollToTopSignal += 1 + } else { + routerPath.path = [] + } + } + } .onChange(of: client.id) { routerPath.path = [] } diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index 3eff2173..7f9a822a 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -28,6 +28,8 @@ struct SettingsTabs: View { @State private var cachedRemoved = false @State private var timelineCache = TimelineCache() + @Binding var popToRootTab: Tab + let isModal: Bool @State private var startingPoint: SettingsStartingPoint? = nil @@ -101,6 +103,11 @@ struct SettingsTabs: View { } .withSafariRouter() .environment(routerPath) + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .notifications { + routerPath.path = [] + } + } } private var accountsSection: some View { @@ -185,6 +192,10 @@ struct SettingsTabs: View { NavigationLink(destination: TabbarEntriesSettingsView()) { Label("settings.general.tabbarEntries", systemImage: "platter.filled.bottom.iphone") } + } else if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + NavigationLink(destination: SidebarEntriesSettingsView()) { + Label("settings.general.sidebarEntries", systemImage: "sidebar.squares.leading") + } } NavigationLink(destination: TranslationSettingsView()) { Label("settings.general.translate", systemImage: "captions.bubble") diff --git a/IceCubesApp/App/Tabs/Settings/SidebarEntriesSettingsView.swift b/IceCubesApp/App/Tabs/Settings/SidebarEntriesSettingsView.swift new file mode 100644 index 00000000..0d1a157f --- /dev/null +++ b/IceCubesApp/App/Tabs/Settings/SidebarEntriesSettingsView.swift @@ -0,0 +1,40 @@ +import DesignSystem +import Env +import SwiftUI + +@MainActor +struct SidebarEntriesSettingsView: View { + @Environment(Theme.self) private var theme + @Environment(UserPreferences.self) private var userPreferences + + @State private var sidebarTabs = SidebarTabs.shared + + var body: some View { + @Bindable var userPreferences = userPreferences + Form { + Section { + ForEach($sidebarTabs.tabs, id: \.tab) { $tab in + if tab.tab != .profile && tab.tab != .settings { + Toggle(isOn: $tab.enabled) { + tab.tab.label + } + } + } + .onMove(perform: move) + } + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif + } + .environment(\.editMode, .constant(.active)) + .navigationTitle("settings.general.sidebarEntries") + #if !os(visionOS) + .scrollContentBackground(.hidden) + .background(theme.secondaryBackgroundColor) + #endif + } + + func move(from source: IndexSet, to destination: Int) { + sidebarTabs.tabs.move(fromOffsets: source, toOffset: destination) + } +} diff --git a/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift index 603cfeb5..1b6c549d 100644 --- a/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TabbarEntriesSettingsView.swift @@ -14,27 +14,27 @@ struct TabbarEntriesSettingsView: View { Form { Section { Picker("settings.tabs.first-tab", selection: $tabs.firstTab) { - ForEach(AppTab.allCases) { tab in + ForEach(Tab.allCases) { tab in tab.label.tag(tab) } } Picker("settings.tabs.second-tab", selection: $tabs.secondTab) { - ForEach(AppTab.allCases) { tab in + ForEach(Tab.allCases) { tab in tab.label.tag(tab) } } Picker("settings.tabs.third-tab", selection: $tabs.thirdTab) { - ForEach(AppTab.allCases) { tab in + ForEach(Tab.allCases) { tab in tab.label.tag(tab) } } Picker("settings.tabs.fourth-tab", selection: $tabs.fourthTab) { - ForEach(AppTab.allCases) { tab in + ForEach(Tab.allCases) { tab in tab.label.tag(tab) } } Picker("settings.tabs.fifth-tab", selection: $tabs.fifthTab) { - ForEach(AppTab.allCases) { tab in + ForEach(Tab.allCases) { tab in tab.label.tag(tab) } } diff --git a/IceCubesApp/App/Tabs/AppTab.swift b/IceCubesApp/App/Tabs/Tabs.swift similarity index 56% rename from IceCubesApp/App/Tabs/AppTab.swift rename to IceCubesApp/App/Tabs/Tabs.swift index 4a06d7a8..7e6ff76c 100644 --- a/IceCubesApp/App/Tabs/AppTab.swift +++ b/IceCubesApp/App/Tabs/Tabs.swift @@ -7,8 +7,8 @@ import StatusKit import SwiftUI @MainActor -enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable { - case timeline, notifications, mentions, explore, messages, settings +enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable { + case timeline, notifications, mentions, explore, messages, settings, other case trending, federated, local case profile case bookmarks @@ -22,43 +22,37 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable { rawValue } - static func loggedOutTab() -> [AppTab] { + static func loggedOutTab() -> [Tab] { [.timeline, .settings] } - static func visionOSTab() -> [AppTab] { + static func visionOSTab() -> [Tab] { [.profile, .timeline, .notifications, .mentions, .explore, .post, .settings] } - - static func sideBarTab() -> [AppTab] { - [.timeline, .trending, .federated, .local, .notifications, - .mentions, .mentions, .explore, .bookmarks, .favorites, - .followedTags, .links, .lists, .settings, .profile] - } @ViewBuilder - func makeContentView(selectedTab: Binding) -> some View { + func makeContentView(selectedTab: Binding, popToRootTab: Binding) -> some View { switch self { case .timeline: - TimelineTab() + TimelineTab(popToRootTab: popToRootTab) case .trending: - TimelineTab(timeline: .trending) + TimelineTab(popToRootTab: popToRootTab, timeline: .trending) case .local: - TimelineTab(timeline: .local) + TimelineTab(popToRootTab: popToRootTab, timeline: .local) case .federated: - TimelineTab(timeline: .federated) + TimelineTab(popToRootTab: popToRootTab, timeline: .federated) case .notifications: - NotificationsTab(selectedTab: selectedTab, lockedType: nil) + NotificationsTab(selectedTab: selectedTab, popToRootTab: popToRootTab, lockedType: nil) case .mentions: - NotificationsTab(selectedTab: selectedTab, lockedType: .mention) + NotificationsTab(selectedTab: selectedTab, popToRootTab: popToRootTab, lockedType: .mention) case .explore: - ExploreTab() + ExploreTab(popToRootTab: popToRootTab) case .messages: - MessagesTab() + MessagesTab(popToRootTab: popToRootTab) case .settings: - SettingsTabs(isModal: false) + SettingsTabs(popToRootTab: popToRootTab, isModal: false) case .profile: - ProfileTab() + ProfileTab(popToRootTab: popToRootTab) case .bookmarks: NavigationTab { AccountStatusesListView(mode: .bookmarks) @@ -79,12 +73,16 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable { NavigationTab { TrendingLinksListView(cards: []) } case .post: VStack {} + case .other: + EmptyView() } } @ViewBuilder var label: some View { - Label(title, systemImage: iconName) + if self != .other { + Label(title, systemImage: iconName) + } } var title: LocalizedStringKey { @@ -121,6 +119,8 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable { "timeline.filter.lists" case .links: "explore.section.trending.links" + case .other: + "" } } @@ -158,10 +158,59 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable { "list.bullet" case .links: "newspaper" + case .other: + "" } } } +@MainActor +@Observable +class SidebarTabs { + struct SidedebarTab: Hashable, Codable { + let tab: Tab + var enabled: Bool + } + + class Storage { + @AppStorage("sidebar_tabs") var tabs: [SidedebarTab] = [ + .init(tab: .timeline, enabled: true), + .init(tab: .trending, enabled: true), + .init(tab: .federated, enabled: true), + .init(tab: .local, enabled: true), + .init(tab: .notifications, enabled: true), + .init(tab: .mentions, enabled: true), + .init(tab: .messages, enabled: true), + .init(tab: .explore, enabled: true), + .init(tab: .bookmarks, enabled: true), + .init(tab: .favorites, enabled: true), + .init(tab: .followedTags, enabled: true), + .init(tab: .lists, enabled: true), + .init(tab: .links, enabled: true), + + .init(tab: .settings, enabled: true), + .init(tab: .profile, enabled: true), + ] + } + + private let storage = Storage() + public static let shared = SidebarTabs() + + var tabs: [SidedebarTab] { + didSet { + storage.tabs = tabs + } + } + + func isEnabled(_ tab: Tab) -> Bool { + tabs.first(where: { $0.tab.id == tab.id })?.enabled == true + } + + private init() { + tabs = storage.tabs + } +} + @MainActor @Observable class iOSTabs { @@ -170,45 +219,45 @@ class iOSTabs { } class Storage { - @AppStorage(TabEntries.first.rawValue) var firstTab = AppTab.timeline - @AppStorage(TabEntries.second.rawValue) var secondTab = AppTab.notifications - @AppStorage(TabEntries.third.rawValue) var thirdTab = AppTab.explore - @AppStorage(TabEntries.fourth.rawValue) var fourthTab = AppTab.links - @AppStorage(TabEntries.fifth.rawValue) var fifthTab = AppTab.profile + @AppStorage(TabEntries.first.rawValue) var firstTab = Tab.timeline + @AppStorage(TabEntries.second.rawValue) var secondTab = Tab.notifications + @AppStorage(TabEntries.third.rawValue) var thirdTab = Tab.explore + @AppStorage(TabEntries.fourth.rawValue) var fourthTab = Tab.links + @AppStorage(TabEntries.fifth.rawValue) var fifthTab = Tab.profile } private let storage = Storage() public static let shared = iOSTabs() - var tabs: [AppTab] { + var tabs: [Tab] { [firstTab, secondTab, thirdTab, fourthTab, fifthTab] } - var firstTab: AppTab { + var firstTab: Tab { didSet { storage.firstTab = firstTab } } - var secondTab: AppTab { + var secondTab: Tab { didSet { storage.secondTab = secondTab } } - var thirdTab: AppTab { + var thirdTab: Tab { didSet { storage.thirdTab = thirdTab } } - var fourthTab: AppTab { + var fourthTab: Tab { didSet { storage.fourthTab = fourthTab } } - var fifthTab: AppTab { + var fifthTab: Tab { didSet { storage.fifthTab = fifthTab } diff --git a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift index 172ed8ba..f4b9852b 100644 --- a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift +++ b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift @@ -18,6 +18,7 @@ struct TimelineTab: View { @Environment(UserPreferences.self) private var preferences @Environment(Client.self) private var client @State private var routerPath = RouterPath() + @Binding var popToRootTab: Tab @State private var didAppear: Bool = false @State private var timeline: TimelineFilter = .home @@ -32,8 +33,9 @@ struct TimelineTab: View { private let canFilterTimeline: Bool - init(timeline: TimelineFilter? = nil) { + init(popToRootTab: Binding, timeline: TimelineFilter? = nil) { canFilterTimeline = timeline == nil + _popToRootTab = popToRootTab _timeline = .init(initialValue: timeline ?? .home) } @@ -75,6 +77,15 @@ struct TimelineTab: View { .onChange(of: currentAccount.account?.id) { resetTimelineFilter() } + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .timeline { + if routerPath.path.isEmpty { + scrollToTopSignal += 1 + } else { + routerPath.path = [] + } + } + } .onChange(of: client.id) { routerPath.path = [] } diff --git a/IceCubesApp/App/Tabs/ToolbarTab.swift b/IceCubesApp/App/Tabs/ToolbarTab.swift index a0b0888c..e28fdd17 100644 --- a/IceCubesApp/App/Tabs/ToolbarTab.swift +++ b/IceCubesApp/App/Tabs/ToolbarTab.swift @@ -15,10 +15,29 @@ struct ToolbarTab: ToolbarContent { var body: some ToolbarContent { if !isSecondaryColumn { + ToolbarItem(placement: .topBarLeading) { + if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + Button { + withAnimation { + userPreferences.isSidebarExpanded.toggle() + } + } label: { + if userPreferences.isSidebarExpanded { + Image(systemName: "sidebar.squares.left") + } else { + Image(systemName: "sidebar.left") + } + } + } + } statusEditorToolbarItem(routerPath: routerPath, visibility: userPreferences.postVisibility) - ToolbarItem(placement: .navigationBarLeading) { - AppAccountsSelectorView(routerPath: routerPath, avatarConfig: theme.avatarShape == .circle ? .badge : .badgeRounded) + if UIDevice.current.userInterfaceIdiom != .pad || + (UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact) + { + ToolbarItem(placement: .navigationBarLeading) { + AppAccountsSelectorView(routerPath: routerPath, avatarConfig: theme.avatarShape == .circle ? .badge : .badgeRounded) + } } } if UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .regular { diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 2e88f5ac..cb964221 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -77330,7 +77330,6 @@ } }, "tab.profile-account-%@" : { - "extractionState" : "stale", "localizations" : { "be" : { "stringUnit" : { @@ -82977,4 +82976,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/IceCubesAppIntents/TabIntent.swift b/IceCubesAppIntents/TabIntent.swift index 7f6fd107..b4bb34ae 100644 --- a/IceCubesAppIntents/TabIntent.swift +++ b/IceCubesAppIntents/TabIntent.swift @@ -35,7 +35,7 @@ enum TabEnum: String, AppEnum, Sendable { .post: .init(title: "New post")] } - var toAppTab: AppTab { + var toAppTab: Tab { switch self { case .timeline: .timeline diff --git a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift index 2f85ad9c..315bb925 100644 --- a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift +++ b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift @@ -23,11 +23,8 @@ struct AccountWidgetProvider: AppIntentTimelineProvider { } private func fetchAccount(configuration: AccountWidgetConfiguration) async -> Account { - guard let account = configuration.account else { - return .placeholder() - } - let client = Client(server: account.account.server, - oauthToken: account.account.oauthToken) + let client = Client(server: configuration.account.account.server, + oauthToken: configuration.account.account.oauthToken) do { let account: Account = try await client.get(endpoint: Accounts.verifyCredentials) return account diff --git a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift index ffd48730..a21f5201 100644 --- a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift @@ -6,7 +6,7 @@ struct AccountWidgetConfiguration: WidgetConfigurationIntent { static let description = IntentDescription("Choose the account for this widget") @Parameter(title: "Account") - var account: AppAccountEntity? + var account: AppAccountEntity } extension AccountWidgetConfiguration { diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift index 42652ed8..82b020d6 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift @@ -29,16 +29,9 @@ struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline { do { - guard let account = configuration.account, let hashgtag = configuration.hashgtag else { - return Timeline(entries: [.init(date: Date(), - title: "#Mastodon", - statuses: [], - images: [:])], - policy: .atEnd) - } - let timeline: TimelineFilter = .hashtag(tag: hashgtag, accountId: nil) + let timeline: TimelineFilter = .hashtag(tag: configuration.hashgtag, accountId: nil) let statuses = await loadStatuses(for: timeline, - account: account, + account: configuration.account, widgetFamily: context.family) let images = try await loadImages(urls: statuses.map { $0.account.avatar }) return Timeline(entries: [.init(date: Date(), diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift index df01d4d0..74d767a9 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift @@ -6,10 +6,10 @@ struct HashtagPostsWidgetConfiguration: WidgetConfigurationIntent { static let description = IntentDescription("Choose the account and hashtag for this widget") @Parameter(title: "Account") - var account: AppAccountEntity? + var account: AppAccountEntity @Parameter(title: "Hashtag") - var hashgtag: String? + var hashgtag: String } extension HashtagPostsWidgetConfiguration { diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift index b7df808e..a79111b5 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift @@ -18,7 +18,7 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { return entry } return .init(date: Date(), - title: configuration.timeline?.timeline.title ?? "", + title: configuration.timeline.timeline.title, statuses: [], images: [:]) } @@ -29,24 +29,17 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async -> Timeline { do { - guard let timeline = configuration.timeline, let account = configuration.account else { - return Timeline(entries: [.init(date: Date(), - title: "", - statuses: [], - images: [:])], - policy: .atEnd) - } - let statuses = await loadStatuses(for: timeline.timeline, - account: account, + let statuses = await loadStatuses(for: configuration.timeline.timeline, + account: configuration.account, widgetFamily: context.family) let images = try await loadImages(urls: statuses.map { $0.account.avatar }) return Timeline(entries: [.init(date: Date(), - title: timeline.timeline.title, + title: configuration.timeline.timeline.title, statuses: statuses, images: images)], policy: .atEnd) } catch { return Timeline(entries: [.init(date: Date(), - title: configuration.timeline?.timeline.title ?? "", + title: configuration.timeline.timeline.title, statuses: [], images: [:])], policy: .atEnd) diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift index 4c30704b..e4dbed5b 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift @@ -6,10 +6,10 @@ struct LatestPostsWidgetConfiguration: WidgetConfigurationIntent { static let description = IntentDescription("Choose the account and timeline for this widget") @Parameter(title: "Account") - var account: AppAccountEntity? + var account: AppAccountEntity @Parameter(title: "Timeline") - var timeline: TimelineFilterEntity? + var timeline: TimelineFilterEntity } extension LatestPostsWidgetConfiguration { diff --git a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift index 42cebd3f..1cc488b2 100644 --- a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift +++ b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidget.swift @@ -29,20 +29,13 @@ struct ListsWidgetProvider: AppIntentTimelineProvider { private func timeline(for configuration: ListsWidgetConfiguration, context: Context) async -> Timeline { do { - guard let account = configuration.account, let timeline = configuration.timeline else { - return Timeline(entries: [.init(date: Date(), - title: "List name", - statuses: [], - images: [:])], - policy: .atEnd) - } - let filter: TimelineFilter = .list(list: timeline.list) - let statuses = await loadStatuses(for: filter, - account: account, + let timeline: TimelineFilter = .list(list: configuration.timeline.list) + let statuses = await loadStatuses(for: timeline, + account: configuration.account, widgetFamily: context.family) let images = try await loadImages(urls: statuses.map { $0.account.avatar }) return Timeline(entries: [.init(date: Date(), - title: filter.title, + title: timeline.title, statuses: statuses, images: images)], policy: .atEnd) } catch { diff --git a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidgetConfiguration.swift index 22f019aa..ebe4e0db 100644 --- a/IceCubesAppWidgetsExtension/ListsWidget/ListsWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/ListsWidget/ListsWidgetConfiguration.swift @@ -6,10 +6,10 @@ struct ListsWidgetConfiguration: WidgetConfigurationIntent { static let description = IntentDescription("Choose the account and list for this widget") @Parameter(title: "Account") - var account: AppAccountEntity? + var account: AppAccountEntity @Parameter(title: "List") - var timeline: ListEntity? + var timeline: ListEntity } extension ListsWidgetConfiguration { diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift index 500a7bd8..a3d2616b 100644 --- a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift @@ -29,15 +29,8 @@ struct MentionsWidgetProvider: AppIntentTimelineProvider { private func timeline(for configuration: MentionsWidgetConfiguration, context _: Context) async -> Timeline { do { - guard let account = configuration.account else { - return Timeline(entries: [.init(date: Date(), - title: "Mentions", - statuses: [], - images: [:])], - policy: .atEnd) - } - let client = Client(server: account.account.server, - oauthToken: account.account.oauthToken) + let client = Client(server: configuration.account.account.server, + oauthToken: configuration.account.account.oauthToken) var excludedTypes = Models.Notification.NotificationType.allCases excludedTypes.removeAll(where: { $0 == .mention }) let notifications: [Models.Notification] = diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift index 3e04faba..9dbe64a3 100644 --- a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift @@ -6,7 +6,7 @@ struct MentionsWidgetConfiguration: WidgetConfigurationIntent { static let description = IntentDescription("Choose the account for this widget") @Parameter(title: "Account") - var account: AppAccountEntity? + var account: AppAccountEntity } extension MentionsWidgetConfiguration { diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift index bed3ec75..2f6ef423 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift @@ -49,21 +49,12 @@ public struct AppAccountsSelectorView: View { .contentShape(Rectangle()) } .sheet(isPresented: $isPresented, content: { - if UIDevice.current.userInterfaceIdiom == .mac || UIDevice.current.userInterfaceIdiom == .pad { - accountsView - .presentationBackground(.ultraThinMaterial) - .onAppear { - refreshAccounts() - } - } else { - accountsView - .presentationDetents([.height(preferredHeight), .large]) - .presentationBackground(.ultraThinMaterial) - .presentationCornerRadius(16) - .onAppear { - refreshAccounts() - } - } + accountsView.presentationDetents([.height(preferredHeight), .large]) + .presentationBackground(.ultraThinMaterial) + .presentationCornerRadius(16) + .onAppear { + refreshAccounts() + } }) .onChange(of: currentAccount.account?.id) { refreshAccounts()