From cf0f0fd8910837de485bf0bb4de6cb8af5f38d28 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Thu, 26 Oct 2023 06:23:00 +0200 Subject: [PATCH] Refactor + add more shortcuts on macOS --- IceCubesApp.xcodeproj/project.pbxproj | 42 ++- IceCubesApp/App/AppRegistry.swift | 2 + IceCubesApp/App/IceCubesApp.swift | 339 ------------------ IceCubesApp/App/Main/IceCubesApp+Menu.swift | 58 +++ IceCubesApp/App/Main/IceCubesApp+Scene.swift | 102 ++++++ .../App/Main/IceCubesApp+Sidebar.swift | 50 +++ IceCubesApp/App/Main/IceCubesApp+Tabbar.swift | 58 +++ IceCubesApp/App/Main/IceCubesApp.swift | 120 +++++++ .../App/Tabs/Timeline/TimelineTab.swift | 16 +- .../Localization/Localizable.xcstrings | 22 ++ .../ShareViewController.swift | 2 +- .../AccountsList/AccountsListRow.swift | 3 +- .../Env/Sources/Env/NotificationsName.swift | 9 +- Packages/Env/Sources/Env/QuickLook.swift | 4 +- Packages/Env/Sources/Env/Router.swift | 12 +- .../Status/Editor/StatusEditorView.swift | 10 +- .../Row/Subviews/StatusRowActionsView.swift | 7 +- .../Row/Subviews/StatusRowContextMenu.swift | 16 +- 18 files changed, 500 insertions(+), 372 deletions(-) delete mode 100644 IceCubesApp/App/IceCubesApp.swift create mode 100644 IceCubesApp/App/Main/IceCubesApp+Menu.swift create mode 100644 IceCubesApp/App/Main/IceCubesApp+Scene.swift create mode 100644 IceCubesApp/App/Main/IceCubesApp+Sidebar.swift create mode 100644 IceCubesApp/App/Main/IceCubesApp+Tabbar.swift create mode 100644 IceCubesApp/App/Main/IceCubesApp.swift diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index fea492b6..0483d576 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -80,6 +80,10 @@ 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 */; }; + 9FB183252AE926E900BBB692 /* IceCubesApp+Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB183242AE926E900BBB692 /* IceCubesApp+Sidebar.swift */; }; + 9FB183272AE9279F00BBB692 /* IceCubesApp+Tabbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB183262AE9279F00BBB692 /* IceCubesApp+Tabbar.swift */; }; + 9FB183292AE9449100BBB692 /* IceCubesApp+Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB183282AE9449100BBB692 /* IceCubesApp+Scene.swift */; }; 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */; }; 9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; }; 9FD34823293D06E800DB0EE9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9FD34822293D06E800DB0EE9 /* Assets.xcassets */; }; @@ -214,6 +218,10 @@ 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 = ""; }; + 9FB183242AE926E900BBB692 /* IceCubesApp+Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceCubesApp+Sidebar.swift"; sourceTree = ""; }; + 9FB183262AE9279F00BBB692 /* IceCubesApp+Tabbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceCubesApp+Tabbar.swift"; sourceTree = ""; }; + 9FB183282AE9449100BBB692 /* IceCubesApp+Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceCubesApp+Scene.swift"; sourceTree = ""; }; 9FBFE639292A715500C250E9 /* IceCubesApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IceCubesApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesApp.swift; sourceTree = ""; }; 9FBFE642292A715600C250E9 /* IceCubesApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesApp.entitlements; sourceTree = ""; }; @@ -337,9 +345,9 @@ 9F398AB429360A5800A889F2 /* App */ = { isa = PBXGroup; children = ( + 9FB183232AE926BB00BBB692 /* Main */, 9F654BF0299AC46200D27FA5 /* Report */, 9FAE4AC9293783A200772766 /* Tabs */, - 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */, 9F398AA52935FE8A00A889F2 /* AppRegistry.swift */, 639CDF9B296AC82F00C35E58 /* SafariRouter.swift */, 9FAD85CE2975B68900496AB1 /* SideBarView.swift */, @@ -418,6 +426,18 @@ path = Tabs; sourceTree = ""; }; + 9FB183232AE926BB00BBB692 /* Main */ = { + isa = PBXGroup; + children = ( + 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */, + 9FB183212AE9268800BBB692 /* IceCubesApp+Menu.swift */, + 9FB183242AE926E900BBB692 /* IceCubesApp+Sidebar.swift */, + 9FB183262AE9279F00BBB692 /* IceCubesApp+Tabbar.swift */, + 9FB183282AE9449100BBB692 /* IceCubesApp+Scene.swift */, + ); + path = Main; + sourceTree = ""; + }; 9FBFE630292A715500C250E9 = { isa = PBXGroup; children = ( @@ -791,16 +811,20 @@ buildActionMask = 2147483647; files = ( 9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */, + 9FB183222AE9268800BBB692 /* IceCubesApp+Menu.swift in Sources */, + 9FB183252AE926E900BBB692 /* IceCubesApp+Sidebar.swift in Sources */, 9F7D939A29805DBD00EE6B7A /* AccountSettingView.swift in Sources */, 069709AA298C9AD7006E4CB5 /* AboutView.swift in Sources */, 9F2B92FC295DA94500DE16D0 /* InstanceInfoView.swift in Sources */, C9B22677297F6C2E001F9EFE /* ContentSettingsView.swift in Sources */, 9FA6FD6229C04A8800E2312C /* TranslationSettingsView.swift in Sources */, + 9FB183272AE9279F00BBB692 /* IceCubesApp+Tabbar.swift in Sources */, 9F35DB4C2952005C00B3281A /* MessagesTab.swift in Sources */, 9FAD85CF2975B68900496AB1 /* SideBarView.swift in Sources */, 9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */, 9F7335F92968576500AFF0BA /* DisplaySettingsView.swift in Sources */, 9F2A540729699698009B2D7C /* SupportAppView.swift in Sources */, + 9FB183292AE9449100BBB692 /* IceCubesApp+Scene.swift in Sources */, 9F2B92F6295AE04800DE16D0 /* Tabs.swift in Sources */, FA31A9AB2A66BF7C00D5F662 /* EditTagGroupView.swift in Sources */, FAD203D02A66D8A80030A7FD /* Symbols.swift in Sources */, @@ -891,7 +915,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.3; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -925,7 +949,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.3; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -960,7 +984,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.3; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -994,7 +1018,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.3; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1174,7 +1198,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.9.3; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -1228,7 +1252,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.9.3; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -1263,7 +1287,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.3; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1298,7 +1322,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.3; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/IceCubesApp/App/AppRegistry.swift b/IceCubesApp/App/AppRegistry.swift index c8928ce1..b0178ad8 100644 --- a/IceCubesApp/App/AppRegistry.swift +++ b/IceCubesApp/App/AppRegistry.swift @@ -10,6 +10,7 @@ import Models import Status import SwiftUI import Timeline +import MediaUI @MainActor extension View { @@ -130,6 +131,7 @@ extension View { .environment(AppAccountsManager.shared) .environment(PushNotificationsService.shared) .environment(AppAccountsManager.shared.currentClient) + .environment(QuickLook.shared) } func withModelContainer() -> some View { diff --git a/IceCubesApp/App/IceCubesApp.swift b/IceCubesApp/App/IceCubesApp.swift deleted file mode 100644 index a88a12fc..00000000 --- a/IceCubesApp/App/IceCubesApp.swift +++ /dev/null @@ -1,339 +0,0 @@ -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 - @Environment(\.openWindow) private var openWindow - - @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(id: "MainWindow") { - 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-window") { - openWindow(id: "MainWindow") - } - .keyboardShortcut("n", modifiers: .shift) - Button("menu.new-post") { - if ProcessInfo.processInfo.isMacCatalystApp { - openWindow(value: WindowDestination.newStatusEditor(visibility: userPreferences.postVisibility)) - } else { - sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) - } - } - .keyboardShortcut("n", modifiers: .command) - } - 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 - } -} diff --git a/IceCubesApp/App/Main/IceCubesApp+Menu.swift b/IceCubesApp/App/Main/IceCubesApp+Menu.swift new file mode 100644 index 00000000..7f311af5 --- /dev/null +++ b/IceCubesApp/App/Main/IceCubesApp+Menu.swift @@ -0,0 +1,58 @@ +import SwiftUI +import Env + +extension IceCubesApp { + @CommandsBuilder + var appMenu: some Commands { + CommandGroup(replacing: .newItem) { + Button("menu.new-window") { + openWindow(id: "MainWindow") + } + .keyboardShortcut("n", modifiers: .shift) + Button("menu.new-post") { + if ProcessInfo.processInfo.isMacCatalystApp { + openWindow(value: WindowDestination.newStatusEditor(visibility: userPreferences.postVisibility)) + } else { + sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) + } + } + .keyboardShortcut("n", modifiers: .command) + } + 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 + } + } + } + } + CommandMenu("tab.timeline") { + Button("timeline.latest") { + NotificationCenter.default.post(name: .refreshTimeline, object: nil) + } + .keyboardShortcut("r", modifiers: .command) + Button("timeline.home") { + NotificationCenter.default.post(name: .homeTimeline, object: nil) + } + .keyboardShortcut("h", modifiers: .shift) + Button("timeline.trending") { + NotificationCenter.default.post(name: .trendingTimeline, object: nil) + } + .keyboardShortcut("t", modifiers: .shift) + Button("timeline.federated") { + NotificationCenter.default.post(name: .federatedTimeline, object: nil) + } + .keyboardShortcut("f", modifiers: .shift) + Button("timeline.local") { + NotificationCenter.default.post(name: .localTimeline, object: nil) + } + .keyboardShortcut("l", modifiers: .shift) + } + } +} diff --git a/IceCubesApp/App/Main/IceCubesApp+Scene.swift b/IceCubesApp/App/Main/IceCubesApp+Scene.swift new file mode 100644 index 00000000..383fcf60 --- /dev/null +++ b/IceCubesApp/App/Main/IceCubesApp+Scene.swift @@ -0,0 +1,102 @@ +import SwiftUI +import Env +import Status +import MediaUI + +extension IceCubesApp { + var appScene: some Scene { + WindowGroup(id: "MainWindow") { + 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 + } + } + + + 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 .editStatusEditor(status): + StatusEditorView(mode: .edit(status: status)) + case let .quoteStatusEditor(status): + StatusEditorView(mode: .quote(status: status)) + case let .replyToStatusEditor(status): + StatusEditorView(mode: .replyTo(status: status)) + case let .mediaViewer(attachments, selectedAttachment): + MediaUIView(selectedAttachment: selectedAttachment, + attachments: attachments) + case .none: + EmptyView() + } + } + .withEnvironments() + .withModelContainer() + .applyTheme(theme) + } + .defaultSize(width: 600, height: 800) + .windowResizability(.automatic) + } +} diff --git a/IceCubesApp/App/Main/IceCubesApp+Sidebar.swift b/IceCubesApp/App/Main/IceCubesApp+Sidebar.swift new file mode 100644 index 00000000..4da9cf4b --- /dev/null +++ b/IceCubesApp/App/Main/IceCubesApp+Sidebar.swift @@ -0,0 +1,50 @@ +import SwiftUI +import Env + +extension IceCubesApp { + 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) + } + + var notificationsSecondaryColumn: some View { + NotificationsTab(popToRootTab: $popToRootTab, lockedType: nil) + .environment(\.isSecondaryColumn, true) + .frame(maxWidth: .secondaryColumnWidth) + .id(appAccountsManager.currentAccount.id) + } + +} diff --git a/IceCubesApp/App/Main/IceCubesApp+Tabbar.swift b/IceCubesApp/App/Main/IceCubesApp+Tabbar.swift new file mode 100644 index 00000000..5b396fb7 --- /dev/null +++ b/IceCubesApp/App/Main/IceCubesApp+Tabbar.swift @@ -0,0 +1,58 @@ +import SwiftUI +import Env + +extension IceCubesApp { + 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 func badgeFor(tab: Tab) -> Int { + if tab == .notifications, selectedTab != tab, + let token = appAccountsManager.currentAccount.oauthToken + { + return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0) + } + return 0 + } +} diff --git a/IceCubesApp/App/Main/IceCubesApp.swift b/IceCubesApp/App/Main/IceCubesApp.swift new file mode 100644 index 00000000..f83261bf --- /dev/null +++ b/IceCubesApp/App/Main/IceCubesApp.swift @@ -0,0 +1,120 @@ +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) var scenePhase + @Environment(\.openWindow) var openWindow + + @State var appAccountsManager = AppAccountsManager.shared + @State var currentInstance = CurrentInstance.shared + @State var currentAccount = CurrentAccount.shared + @State var userPreferences = UserPreferences.shared + @State var pushNotificationsService = PushNotificationsService.shared + @State var watcher = StreamWatcher() + @State var quickLook = QuickLook.shared + @State var theme = Theme.shared + @State var sidebarRouterPath = RouterPath() + + @State var selectedTab: Tab = .timeline + @State var popToRootTab: Tab = .other + @State var sideBarLoadedTabs: Set = Set() + @State var isSupporter: Bool = false + + var availableTabs: [Tab] { + appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab() + } + + var body: some Scene { + appScene + otherScenes + } + + 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]) + } + } + + 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 + } + } + + func setupRevenueCat() { + Purchases.logLevel = .error + Purchases.configure(withAPIKey: "appl_JXmiRckOzXXTsHKitQiicXCvMQi") + Purchases.shared.getCustomerInfo { info, _ in + if info?.entitlements["Supporter"]?.isActive == true { + isSupporter = true + } + } + } + + func refreshPushSubs() { + PushNotificationsService.shared.requestPushNotifications() + } +} + +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 + } +} diff --git a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift index a101179a..1cc10f77 100644 --- a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift +++ b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift @@ -98,6 +98,21 @@ struct TimelineTab: View { selectedTagGroup = nil } } + .onReceive(NotificationCenter.default.publisher(for: .refreshTimeline)) { _ in + timeline = .latest + } + .onReceive(NotificationCenter.default.publisher(for: .trendingTimeline)) { _ in + timeline = .trending + } + .onReceive(NotificationCenter.default.publisher(for: .localTimeline)) { _ in + timeline = .local + } + .onReceive(NotificationCenter.default.publisher(for: .federatedTimeline)) { _ in + timeline = .federated + } + .onReceive(NotificationCenter.default.publisher(for: .homeTimeline)) { _ in + timeline = .home + } .withSafariRouter() .environment(routerPath) } @@ -110,7 +125,6 @@ struct TimelineTab: View { } label: { Label(TimelineFilter.latest.localizedTitle(), systemImage: TimelineFilter.latest.iconName() ?? "") } - .keyboardShortcut("r", modifiers: .command) Divider() } ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 872af716..295fd31d 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -30068,6 +30068,28 @@ } } }, + "menu.timeline" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Timeline" + } + } + } + }, + "menu.timeline.refresh" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jump to Latest" + } + } + } + }, "notifications-others-count %lld" : { "extractionState" : "manual", "localizations" : { diff --git a/IceCubesShareExtension/ShareViewController.swift b/IceCubesShareExtension/ShareViewController.swift index 95353983..5d217184 100644 --- a/IceCubesShareExtension/ShareViewController.swift +++ b/IceCubesShareExtension/ShareViewController.swift @@ -52,7 +52,7 @@ class ShareViewController: UIViewController { } } - NotificationCenter.default.addObserver(forName: NotificationsName.shareSheetClose, + NotificationCenter.default.addObserver(forName: .shareSheetClose, object: nil, queue: nil) { [weak self] _ in diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift index 9f31090d..dd1b7df4 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift @@ -26,6 +26,7 @@ public struct AccountsListRow: View { @Environment(CurrentAccount.self) private var currentAccount @Environment(RouterPath.self) private var routerPath @Environment(Client.self) private var client + @Environment(QuickLook.self) private var quickLook @State var viewModel: AccountsListRowViewModel @@ -122,7 +123,7 @@ public struct AccountsListRow: View { .environment(theme) .environment(currentAccount) .environment(client) - .environment(QuickLook()) + .environment(quickLook) .environment(routerPath) } } diff --git a/Packages/Env/Sources/Env/NotificationsName.swift b/Packages/Env/Sources/Env/NotificationsName.swift index 2cf24be5..d740f7f8 100644 --- a/Packages/Env/Sources/Env/NotificationsName.swift +++ b/Packages/Env/Sources/Env/NotificationsName.swift @@ -1,5 +1,10 @@ import UIKit -public enum NotificationsName { - public static let shareSheetClose = NSNotification.Name("shareSheetClose") +public extension Notification.Name { + static let shareSheetClose = NSNotification.Name("shareSheetClose") + static let refreshTimeline = Notification.Name("refreshTimeline") + static let homeTimeline = Notification.Name("homeTimeline") + static let trendingTimeline = Notification.Name("trendingTimeline") + static let federatedTimeline = Notification.Name("federatedTimeline") + static let localTimeline = Notification.Name("localTimeline") } diff --git a/Packages/Env/Sources/Env/QuickLook.swift b/Packages/Env/Sources/Env/QuickLook.swift index fb5a991f..b3e89ffb 100644 --- a/Packages/Env/Sources/Env/QuickLook.swift +++ b/Packages/Env/Sources/Env/QuickLook.swift @@ -7,7 +7,9 @@ import QuickLook public var selectedMediaAttachment: MediaAttachment? public var mediaAttachments: [MediaAttachment] = [] - public init() {} + public static let shared = QuickLook() + + private init() {} public func prepareFor(selectedMediaAttachment: MediaAttachment, mediaAttachments: [MediaAttachment]) { self.selectedMediaAttachment = selectedMediaAttachment diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index fce330b3..23052bb2 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -28,15 +28,9 @@ public enum RouterDestination: Hashable { public enum WindowDestination: Hashable, Codable { case newStatusEditor(visibility: Models.Visibility) case mediaViewer(attachments: [MediaAttachment], selectedAttachment: MediaAttachment) - - var initialSize: CGSize { - switch self { - case .newStatusEditor: - return .init(width: 500, height: 700) - case .mediaViewer: - return .init(width: 800, height: 600) - } - } + case editStatusEditor(status: Status) + case replyToStatusEditor(status: Status) + case quoteStatusEditor(status: Status) } public enum SheetDestination: Identifiable { diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index 89ee291a..b6e2c856 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -89,7 +89,7 @@ public struct StatusEditorView: View { viewModel.prepareStatusText() if !client.isAuth { dismiss() - NotificationCenter.default.post(name: NotificationsName.shareSheetClose, + NotificationCenter.default.post(name: .shareSheetClose, object: nil) } @@ -141,7 +141,7 @@ public struct StatusEditorView: View { isDismissAlertPresented = true } else { dismiss() - NotificationCenter.default.post(name: NotificationsName.shareSheetClose, + NotificationCenter.default.post(name: .shareSheetClose, object: nil) } } label: { @@ -154,13 +154,13 @@ public struct StatusEditorView: View { actions: { Button("status.draft.delete", role: .destructive) { dismiss() - NotificationCenter.default.post(name: NotificationsName.shareSheetClose, + NotificationCenter.default.post(name: .shareSheetClose, object: nil) } Button("status.draft.save") { context.insert(Draft(content: viewModel.statusText.string)) dismiss() - NotificationCenter.default.post(name: NotificationsName.shareSheetClose, + NotificationCenter.default.post(name: .shareSheetClose, object: nil) } Button("action.cancel", role: .cancel) {} @@ -213,7 +213,7 @@ public struct StatusEditorView: View { if status != nil { dismiss() SoundEffectManager.shared.playSound(of: .tootSent) - NotificationCenter.default.post(name: NotificationsName.shareSheetClose, + NotificationCenter.default.post(name: .shareSheetClose, object: nil) if !viewModel.mode.isInShareExtension, !preferences.requestedReview, !ProcessInfo.processInfo.isMacCatalystApp { if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift index 8548213c..d887cd17 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift @@ -11,6 +11,7 @@ struct StatusRowActionsView: View { @Environment(StatusDataController.self) private var statusDataController @Environment(UserPreferences.self) private var userPreferences + @Environment(\.openWindow) private var openWindow @Environment(\.isStatusFocused) private var isFocused var viewModel: StatusRowViewModel @@ -204,7 +205,11 @@ struct StatusRowActionsView: View { switch action { case .respond: SoundEffectManager.shared.playSound(of: .share) - viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status) + if ProcessInfo.processInfo.isMacCatalystApp { + openWindow(value: WindowDestination.replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status)) + } else { + viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status) + } case .favorite: SoundEffectManager.shared.playSound(of: .favorite) await statusDataController.toggleFavorite(remoteStatus: viewModel.localStatusId) diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift index 8ead9d92..2186c7d6 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift @@ -7,6 +7,7 @@ import SwiftUI @MainActor struct StatusRowContextMenu: View { @Environment(\.displayScale) var displayScale + @Environment(\.openWindow) var openWindow @Environment(Client.self) private var client @Environment(SceneDelegate.self) private var sceneDelegate @@ -14,6 +15,7 @@ struct StatusRowContextMenu: View { @Environment(CurrentAccount.self) private var account @Environment(CurrentInstance.self) private var currentInstance @Environment(StatusDataController.self) private var statusDataController + @Environment(QuickLook.self) private var quickLook var viewModel: StatusRowViewModel @@ -51,12 +53,20 @@ struct StatusRowContextMenu: View { systemImage: "bookmark") } Button { - viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status) + if ProcessInfo.processInfo.isMacCatalystApp { + openWindow(value: WindowDestination.replyToStatusEditor(status: viewModel.status)) + } else { + viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status) + } } label: { Label("status.action.reply", systemImage: "arrowshape.turn.up.left") } Button { - viewModel.routerPath.presentedSheet = .quoteStatusEditor(status: viewModel.status) + if ProcessInfo.processInfo.isMacCatalystApp { + openWindow(value: WindowDestination.quoteStatusEditor(status: viewModel.status)) + } else { + viewModel.routerPath.presentedSheet = .quoteStatusEditor(status: viewModel.status) + } } label: { Label("status.action.quote", systemImage: "quote.bubble") } @@ -91,7 +101,7 @@ struct StatusRowContextMenu: View { .environment(account) .environment(currentInstance) .environment(SceneDelegate()) - .environment(QuickLook()) + .environment(quickLook) .environment(viewModel.client) .preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light) .foregroundColor(Theme.shared.labelColor)