import Account import AppAccount import AVFoundation import DesignSystem import Env import KeychainSwift import Network import RevenueCat import Status import SwiftUI import Timeline import MediaUI @main struct IceCubesApp: App { @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate @Environment(\.scenePhase) private var scenePhase @State private var appAccountsManager = AppAccountsManager.shared @State private var currentInstance = CurrentInstance.shared @State private var currentAccount = CurrentAccount.shared @State private var userPreferences = UserPreferences.shared @State private var pushNotificationsService = PushNotificationsService.shared @State private var watcher = StreamWatcher() @State private var quickLook = QuickLook() @State private var theme = Theme.shared @State private var sidebarRouterPath = RouterPath() @State private var selectedTab: Tab = .timeline @State private var popToRootTab: Tab = .other @State private var sideBarLoadedTabs: Set = Set() @State private var isSupporter: Bool = false private var availableTabs: [Tab] { appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab() } var body: some Scene { appScene otherScenes } private var appScene: some Scene { WindowGroup { appView .applyTheme(theme) .onAppear { setNewClientsInEnv(client: appAccountsManager.currentClient) setupRevenueCat() refreshPushSubs() } .environment(appAccountsManager) .environment(appAccountsManager.currentClient) .environment(quickLook) .environment(currentAccount) .environment(currentInstance) .environment(userPreferences) .environment(theme) .environment(watcher) .environment(pushNotificationsService) .environment(\.isSupporter, isSupporter) .sheet(item: $quickLook.selectedMediaAttachment) { selectedMediaAttachment in MediaUIView(selectedAttachment: selectedMediaAttachment, attachments: quickLook.mediaAttachments) .presentationBackground(.ultraThinMaterial) .presentationCornerRadius(16) .withEnvironments() } .onChange(of: pushNotificationsService.handledNotification) { _, newValue in if newValue != nil { pushNotificationsService.handledNotification = nil if appAccountsManager.currentAccount.oauthToken?.accessToken != newValue?.account.token.accessToken, let account = appAccountsManager.availableAccounts.first(where: { $0.oauthToken?.accessToken == newValue?.account.token.accessToken }) { appAccountsManager.currentAccount = account DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { selectedTab = .notifications pushNotificationsService.handledNotification = newValue } } else { selectedTab = .notifications } } } .withModelContainer() } .commands { appMenu } .onChange(of: scenePhase) { _, newValue in handleScenePhase(scenePhase: newValue) } .onChange(of: appAccountsManager.currentClient) { _, newValue in setNewClientsInEnv(client: newValue) if newValue.isAuth { watcher.watch(streams: [.user, .direct]) } } } @ViewBuilder private var appView: some View { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { sidebarView } else { tabBarView } } private func badgeFor(tab: Tab) -> Int { if tab == .notifications, selectedTab != tab, let token = appAccountsManager.currentAccount.oauthToken { return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0) } return 0 } private var sidebarView: some View { SideBarView(selectedTab: $selectedTab, popToRootTab: $popToRootTab, tabs: availableTabs) { HStack(spacing: 0) { ZStack { if selectedTab == .profile { ProfileTab(popToRootTab: $popToRootTab) } ForEach(availableTabs) { tab in if tab == selectedTab || sideBarLoadedTabs.contains(tab) { tab .makeContentView(popToRootTab: $popToRootTab) .opacity(tab == selectedTab ? 1 : 0) .transition(.opacity) .id("\(tab)\(appAccountsManager.currentAccount.id)") .onAppear { sideBarLoadedTabs.insert(tab) } } else { EmptyView() } } } if appAccountsManager.currentClient.isAuth, userPreferences.showiPadSecondaryColumn { Divider().edgesIgnoringSafeArea(.all) notificationsSecondaryColumn } } }.onChange(of: $appAccountsManager.currentAccount.id) { sideBarLoadedTabs.removeAll() } .environment(sidebarRouterPath) } private var notificationsSecondaryColumn: some View { NotificationsTab(popToRootTab: $popToRootTab, lockedType: nil) .environment(\.isSecondaryColumn, true) .frame(maxWidth: .secondaryColumnWidth) .id(appAccountsManager.currentAccount.id) } private var tabBarView: some View { TabView(selection: .init(get: { selectedTab }, set: { newTab in 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(of: .tabSelection) SoundEffectManager.shared.playSound(of: .tabSelection) selectedTab = newTab DispatchQueue.main.async { if selectedTab == .notifications, let token = appAccountsManager.currentAccount.oauthToken { userPreferences.notificationsCount[token] = 0 watcher.unreadNotificationsCount = 0 } } })) { ForEach(availableTabs) { tab in tab.makeContentView(popToRootTab: $popToRootTab) .tabItem { if userPreferences.showiPhoneTabLabel { tab.label } else { Image(systemName: tab.iconName) } } .tag(tab) .badge(badgeFor(tab: tab)) .toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .tabBar) } } .id(appAccountsManager.currentClient.id) } private var otherScenes: some Scene { WindowGroup(for: WindowDestination.self) { destination in Group { switch destination.wrappedValue { case let .newStatusEditor(visibility): StatusEditorView(mode: .new(visibility: visibility)) case let .mediaViewer(attachments, selectedAttachment): MediaUIView(selectedAttachment: selectedAttachment, attachments: attachments) case .none: EmptyView() } } .withEnvironments() .withModelContainer() .applyTheme(theme) } .defaultSize(width: 600, height: 800) .windowResizability(.automatic) } private func setNewClientsInEnv(client: Client) { currentAccount.setClient(client: client) currentInstance.setClient(client: client) userPreferences.setClient(client: client) Task { await currentInstance.fetchCurrentInstance() watcher.setClient(client: client, instanceStreamingURL: currentInstance.instance?.urls?.streamingApi) watcher.watch(streams: [.user, .direct]) } } private func handleScenePhase(scenePhase: ScenePhase) { switch scenePhase { case .background: watcher.stopWatching() case .active: watcher.watch(streams: [.user, .direct]) UNUserNotificationCenter.current().setBadgeCount(0) userPreferences.reloadNotificationsCount(tokens: appAccountsManager.availableAccounts.compactMap(\.oauthToken)) Task { await userPreferences.refreshServerPreferences() } default: break } } private func setupRevenueCat() { Purchases.logLevel = .error Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi") Purchases.shared.getCustomerInfo { info, _ in if info?.entitlements["Supporter"]?.isActive == true { isSupporter = true } } } private func refreshPushSubs() { PushNotificationsService.shared.requestPushNotifications() } @CommandsBuilder private var appMenu: some Commands { CommandGroup(replacing: .newItem) { Button("menu.new-post") { sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) } } CommandGroup(replacing: .textFormatting) { Menu("menu.font") { Button("menu.font.bigger") { if theme.fontSizeScale < 1.5 { theme.fontSizeScale += 0.1 } } Button("menu.font.smaller") { if theme.fontSizeScale > 0.5 { theme.fontSizeScale -= 0.1 } } } } } } class AppDelegate: NSObject, UIApplicationDelegate { func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { try? AVAudioSession.sharedInstance().setCategory(.ambient) PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts) return true } func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { PushNotificationsService.shared.pushToken = deviceToken Task { PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts) await PushNotificationsService.shared.updateSubscriptions(forceCreate: false) } } func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {} func application(_: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any]) async -> UIBackgroundFetchResult { UserPreferences.shared.reloadNotificationsCount(tokens: AppAccountsManager.shared.availableAccounts.compactMap(\.oauthToken)) return .noData } func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) if connectingSceneSession.role == .windowApplication { configuration.delegateClass = SceneDelegate.self } return configuration } }