diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift index 5caac7432..b5400ac1d 100644 --- a/Mac/AppDefaults.swift +++ b/Mac/AppDefaults.swift @@ -15,9 +15,9 @@ enum FontSize: Int { } final class AppDefaults { - + static let defaultThemeName = "Default" - + static var shared = AppDefaults() private init() {} @@ -68,7 +68,7 @@ final class AppDefaults { } return false }() - + var isFirstRun: Bool = { if let _ = UserDefaults.standard.object(forKey: Key.firstRunDate) as? Date { return false @@ -76,7 +76,7 @@ final class AppDefaults { firstRunDate = Date() return true }() - + var windowState: [AnyHashable : Any]? { get { return UserDefaults.standard.object(forKey: Key.windowState) as? [AnyHashable : Any] @@ -85,7 +85,7 @@ final class AppDefaults { UserDefaults.standard.set(newValue, forKey: Key.windowState) } } - + var activeExtensionPointIDs: [[AnyHashable : AnyHashable]]? { get { return UserDefaults.standard.object(forKey: Key.activeExtensionPointIDs) as? [[AnyHashable : AnyHashable]] @@ -94,7 +94,7 @@ final class AppDefaults { UserDefaults.standard.set(newValue, forKey: Key.activeExtensionPointIDs) } } - + var lastImageCacheFlushDate: Date? { get { return AppDefaults.date(for: Key.lastImageCacheFlushDate) @@ -103,7 +103,7 @@ final class AppDefaults { AppDefaults.setDate(for: Key.lastImageCacheFlushDate, newValue) } } - + var openInBrowserInBackground: Bool { get { return AppDefaults.bool(for: Key.openInBrowserInBackground) @@ -169,7 +169,7 @@ final class AppDefaults { AppDefaults.setString(for: Key.addWebFeedAccountID, newValue) } } - + var addWebFeedFolderName: String? { get { return AppDefaults.string(for: Key.addWebFeedFolderName) @@ -187,7 +187,7 @@ final class AppDefaults { AppDefaults.setString(for: Key.addFolderAccountID, newValue) } } - + var importOPMLAccountID: String? { get { return AppDefaults.string(for: Key.importOPMLAccountID) @@ -196,7 +196,7 @@ final class AppDefaults { AppDefaults.setString(for: Key.importOPMLAccountID, newValue) } } - + var exportOPMLAccountID: String? { get { return AppDefaults.string(for: Key.exportOPMLAccountID) @@ -214,7 +214,7 @@ final class AppDefaults { AppDefaults.setString(for: Key.defaultBrowserID, newValue) } } - + var currentThemeName: String? { get { return AppDefaults.string(for: Key.currentThemeName) @@ -232,7 +232,7 @@ final class AppDefaults { UserDefaults.standard.set(newValue, forKey: Key.hasSeenNotAllArticlesHaveURLsAlert) } } - + var showTitleOnMainWindow: Bool { return AppDefaults.bool(for: Key.showTitleOnMainWindow) } @@ -287,7 +287,7 @@ final class AppDefaults { AppDefaults.setSortDirection(for: Key.timelineSortDirection, newValue) } } - + var timelineGroupByFeed: Bool { get { return AppDefaults.bool(for: Key.timelineGroupByFeed) @@ -296,7 +296,7 @@ final class AppDefaults { AppDefaults.setBool(for: Key.timelineGroupByFeed, newValue) } } - + var timelineShowsSeparators: Bool { return AppDefaults.bool(for: Key.timelineShowsSeparators) } @@ -320,7 +320,7 @@ final class AppDefaults { UserDefaults.standard.set(newValue.rawValue, forKey: Key.refreshInterval) } } - + var twitterDeprecationAlertShown: Bool { get { return AppDefaults.bool(for: Key.twitterDeprecationAlertShown) @@ -329,7 +329,7 @@ final class AppDefaults { AppDefaults.setBool(for: Key.twitterDeprecationAlertShown, newValue) } } - + var markArticlesAsReadOnScroll: Bool { get { return AppDefaults.bool(for: Key.markArticlesAsReadOnScroll) @@ -410,19 +410,19 @@ private extension AppDefaults { // } // return FontSize(rawValue: rawFontSize)! } - + static func setFontSize(for key: String, _ fontSize: FontSize) { setInt(for: key, fontSize.rawValue) } - + static func string(for key: String) -> String? { return UserDefaults.standard.string(forKey: key) } - + static func setString(for key: String, _ value: String?) { UserDefaults.standard.set(value, forKey: key) } - + static func bool(for key: String) -> Bool { return UserDefaults.standard.bool(forKey: key) } @@ -434,11 +434,11 @@ private extension AppDefaults { static func int(for key: String) -> Int { return UserDefaults.standard.integer(forKey: key) } - + static func setInt(for key: String, _ x: Int) { UserDefaults.standard.set(x, forKey: key) } - + static func date(for key: String) -> Date? { return UserDefaults.standard.object(forKey: key) as? Date } diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 16ec251f3..e1f7ddc47 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -32,11 +32,11 @@ var appDelegate: AppDelegate! @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate, Logging { - + private struct WindowRestorationIdentifiers { static let mainWindow = "mainWindow" } - + var userNotificationManager: UserNotificationManager! var faviconDownloader: FaviconDownloader! var imageDownloader: ImageDownloader! @@ -44,13 +44,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, var webFeedIconDownloader: WebFeedIconDownloader! var extensionContainersFile: ExtensionContainersFile! var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile! - + var appName: String! - + var refreshTimer: AccountRefreshTimer? var syncTimer: ArticleStatusSyncTimer? var lastRefreshInterval = AppDefaults.shared.refreshInterval - + var shuttingDown = false { didSet { if shuttingDown { @@ -61,9 +61,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } } - + var isShutDownSyncDone = false - + @IBOutlet var shareMenuItem: NSMenuItem! @IBOutlet var fileMenuItem: NSMenuItem! @IBOutlet var debugMenuItem: NSMenuItem! @@ -71,7 +71,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @IBOutlet var sortByNewestArticleOnTopMenuItem: NSMenuItem! @IBOutlet var groupArticlesByFeedMenuItem: NSMenuItem! @IBOutlet var checkForUpdatesMenuItem: NSMenuItem! - + var unreadCount = 0 { didSet { if unreadCount != oldValue { @@ -80,7 +80,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } } - + private var mainWindowController: MainWindowController? { var bestController: MainWindowController? for candidateController in mainWindowControllers { @@ -94,7 +94,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } return bestController } - + private var mainWindowControllers = [MainWindowController]() private var preferencesWindowController: NSWindowController? private var addFeedController: AddFeedController? @@ -109,50 +109,50 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, private var softwareUpdater: SPUUpdater! private var crashReporter: PLCrashReporter! #endif - + @MainActor override init() { NSWindow.allowsAutomaticWindowTabbing = false super.init() - + #if !MAC_APP_STORE let crashReporterConfig = PLCrashReporterConfig.defaultConfiguration() crashReporter = PLCrashReporter(configuration: crashReporterConfig) crashReporter.enable() #endif - + SecretsManager.provider = Secrets() AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!) ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!) FeedProviderManager.shared.delegate = ExtensionPointManager.shared - + NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil) NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil) - + appDelegate = self - + presentTwitterDeprecationAlertIfRequired() } - + // MARK: - API @MainActor func showAddFolderSheetOnWindow(_ window: NSWindow) { addFolderWindowController = AddFolderWindowController() addFolderWindowController!.runSheetOnWindow(window) } - + @MainActor func showAddWebFeedSheetOnWindow(_ window: NSWindow, urlString: String?, name: String?, account: Account?, folder: Folder?) { addFeedController = AddFeedController(hostWindow: window) addFeedController?.showAddFeedSheet(.webFeed, urlString, name, account, folder) } - + // MARK: - NSApplicationDelegate - + @MainActor func applicationWillFinishLaunching(_ notification: Notification) { installAppleEventHandlers() - + CacheCleaner.purgeIfNecessary() - + // Try to establish a cache in the Caches folder, but if it fails for some reason fall back to a temporary dir let cacheFolder: String if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path { @@ -162,25 +162,25 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String) cacheFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier) } - + let faviconsFolder = (cacheFolder as NSString).appendingPathComponent("Favicons") let faviconsFolderURL = URL(fileURLWithPath: faviconsFolder) try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil) faviconDownloader = FaviconDownloader(folder: faviconsFolder) - + let imagesFolder = (cacheFolder as NSString).appendingPathComponent("Images") let imagesFolderURL = URL(fileURLWithPath: imagesFolder) try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil) imageDownloader = ImageDownloader(folder: imagesFolder) - + authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader) webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder) - + appName = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String) } - + @MainActor func applicationDidFinishLaunching(_ note: Notification) { - + #if MAC_APP_STORE || TEST checkForUpdatesMenuItem.isHidden = true #else @@ -188,7 +188,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let hostBundle = Bundle.main let updateDriver = SPUStandardUserDriver(hostBundle: hostBundle, delegate: self) self.softwareUpdater = SPUUpdater(hostBundle: hostBundle, applicationBundle: hostBundle, userDriver: updateDriver, delegate: self) - + do { try self.softwareUpdater.start() } @@ -196,55 +196,55 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, logger.error("Failed to start software updater with error: \(error.localizedDescription, privacy: .public)") } #endif - + AppDefaults.shared.registerDefaults() let isFirstRun = AppDefaults.shared.isFirstRun if isFirstRun { logger.debug("Is first run") } let localAccount = AccountManager.shared.defaultAccount - + if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() { // Import feeds. Either old NNW 3 feeds or the default feeds. if !NNW3ImportController.importSubscriptionsIfFileExists(account: localAccount) { DefaultFeedsImporter.importDefaultFeeds(account: localAccount) } } - + updateSortMenuItems() updateGroupByFeedMenuItem() - + if mainWindowController == nil { let mainWindowController = createAndShowMainWindow() mainWindowController.restoreStateFromUserDefaults() } - + fileMenuItem.submenu?.delegate = self shareMenuItem.submenu?.delegate = self - + if isFirstRun { mainWindowController?.window?.center() } - + NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) - + DispatchQueue.main.async { self.unreadCount = AccountManager.shared.unreadCount } - + if InspectorWindowController.shouldOpenAtStartup { self.toggleInspectorWindow(self) } - + extensionContainersFile = ExtensionContainersFile() extensionFeedAddRequestFile = ExtensionFeedAddRequestFile() - + refreshTimer = AccountRefreshTimer() syncTimer = ArticleStatusSyncTimer() - + UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .alert, .sound]) { (granted, error) in } - + UNUserNotificationCenter.current().getNotificationSettings { (settings) in if settings.authorizationStatus == .authorized { DispatchQueue.main.async { @@ -252,10 +252,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } } - + UNUserNotificationCenter.current().delegate = self userNotificationManager = UserNotificationManager() - + #if DEBUG refreshTimer!.update() syncTimer!.update() @@ -270,7 +270,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } #endif - + if AppDefaults.shared.showDebugMenu { // The Web Inspector uses SPI and can never appear in a MAC_APP_STORE build. #if MAC_APP_STORE @@ -283,15 +283,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } else { debugMenuItem.menu?.removeItem(debugMenuItem) } - + #if !MAC_APP_STORE DispatchQueue.main.async { CrashReporter.check(crashReporter: self.crashReporter) } #endif - + } - + @MainActor func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool { guard let mainWindowController = mainWindowController else { return false @@ -299,7 +299,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, mainWindowController.handle(userActivity) return true } - + @MainActor func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { // https://github.com/brentsimmons/NetNewsWire/issues/522 // I couldn’t reproduce the crashing bug, but it appears to happen on creating a main window @@ -313,43 +313,43 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, mainWindowController.showWindow(self) return false } - + @MainActor func applicationDidBecomeActive(_ notification: Notification) { fireOldTimers() } - + @MainActor func applicationDidResignActive(_ notification: Notification) { ArticleStringFormatter.emptyCaches() saveState() } - + @MainActor func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) } - + @MainActor func application(_ sender: NSApplication, openFile filename: String) -> Bool { guard filename.hasSuffix(ArticleTheme.nnwThemeSuffix) else { return false } importTheme(filename: filename) return true } - + @MainActor func applicationWillTerminate(_ notification: Notification) { shuttingDown = true saveState() - + ArticleThemeDownloader.shared.cleanUp() - + AccountManager.shared.sendArticleStatusAll() { self.isShutDownSyncDone = true } - + let timeout = Date().addingTimeInterval(2) while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: timeout) && timeout > Date() { } } - + @MainActor func presentThemeImportError(_ error: Error) { var informativeText: String = "" - + if let decodingError = error as? DecodingError { switch decodingError { case .typeMismatch(let type, _): @@ -369,35 +369,35 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } let localizedError = NSLocalizedString("This theme cannot be used because of data corruption in the Info.plist: %@.", comment: "Decoding key missing") informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String - + default: informativeText = error.localizedDescription } } else { informativeText = error.localizedDescription } - + DispatchQueue.main.async { let alert = NSAlert() alert.alertStyle = .warning alert.messageText = NSLocalizedString("Theme Error", comment: "Theme error") alert.informativeText = informativeText alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) - + alert.buttons[0].keyEquivalent = "\r" - + _ = alert.runModal() } } - + // MARK: Notifications - + @MainActor @objc func unreadCountDidChange(_ note: Notification) { if note.object is AccountManager { unreadCount = AccountManager.shared.unreadCount } } - + @MainActor @objc func webFeedSettingDidChange(_ note: Notification) { guard let feed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else { return @@ -406,30 +406,30 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let _ = faviconDownloader.favicon(for: feed) } } - + @MainActor @objc func inspectableObjectsDidChange(_ note: Notification) { guard let inspectorWindowController = inspectorWindowController, inspectorWindowController.isOpen else { return } inspectorWindowController.objects = objectsForInspector() } - + @MainActor @objc func userDefaultsDidChange(_ note: Notification) { updateSortMenuItems() updateGroupByFeedMenuItem() - + if lastRefreshInterval != AppDefaults.shared.refreshInterval { refreshTimer?.update() lastRefreshInterval = AppDefaults.shared.refreshInterval } - + updateDockBadge() } - + @MainActor @objc func didWakeNotification(_ note: Notification) { fireOldTimers() } - + @MainActor @objc func importDownloadedTheme(_ note: Notification) { guard let userInfo = note.userInfo, let url = userInfo["url"] as? URL else { @@ -439,38 +439,38 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, self.importTheme(filename: url.path) } } - + // MARK: Main Window - + @MainActor func createMainWindowController() -> MainWindowController { let controller: MainWindowController controller = windowControllerWithName("MainWindow") as! MainWindowController - + if !(mainWindowController?.isOpen ?? false) { mainWindowControllers.removeAll() } mainWindowControllers.append(controller) return controller } - + @MainActor func windowControllerWithName(_ storyboardName: String) -> NSWindowController { let storyboard = NSStoryboard(name: NSStoryboard.Name(storyboardName), bundle: nil) return storyboard.instantiateInitialController()! as! NSWindowController } - + @discardableResult @MainActor func createAndShowMainWindow() -> MainWindowController { let controller = createMainWindowController() controller.showWindow(self) - + if let window = controller.window { window.restorationClass = Self.self window.identifier = NSUserInterfaceItemIdentifier(rawValue: WindowRestorationIdentifiers.mainWindow) } - + return controller } - + @MainActor func createAndShowMainWindowIfNecessary() { if mainWindowController == nil { createAndShowMainWindow() @@ -478,69 +478,69 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, mainWindowController?.showWindow(self) } } - + @MainActor func removeMainWindow(_ windowController: MainWindowController) { guard mainWindowControllers.count > 1 else { return } if let index = mainWindowControllers.firstIndex(of: windowController) { mainWindowControllers.remove(at: index) } } - + // MARK: NSUserInterfaceValidations @MainActor func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { if shuttingDown { return false } - + let isDisplayingSheet = mainWindowController?.isDisplayingSheet ?? false let isSpecialAccountAvailable = AccountManager.shared.activeAccounts.contains(where: { $0.type == .onMyMac || $0.type == .cloudKit }) - + if item.action == #selector(refreshAll(_:)) { return !AccountManager.shared.refreshInProgress && !AccountManager.shared.activeAccounts.isEmpty } - + if item.action == #selector(importOPMLFromFile(_:)) { return AccountManager.shared.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) }) } - + if item.action == #selector(addAppNews(_:)) { return !isDisplayingSheet && !AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() && !AccountManager.shared.activeAccounts.isEmpty } - + if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) { return mainWindowController?.isOpen ?? false } - + if item.action == #selector(showAddWebFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) { return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty } - + if item.action == #selector(showAddRedditFeedWindow(_:)) { guard !isDisplayingSheet && isSpecialAccountAvailable && ExtensionPointManager.shared.isRedditEnabled else { return false } return ExtensionPointManager.shared.isRedditEnabled } - + #if !MAC_APP_STORE if item.action == #selector(toggleWebInspectorEnabled(_:)) { (item as! NSMenuItem).state = AppDefaults.shared.webInspectorEnabled ? .on : .off } #endif - + return true } // MARK: UNUserNotificationCenterDelegate - + @MainActor func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .badge, .sound]) } - + @MainActor func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - + let userInfo = response.notification.request.content.userInfo - + switch response.actionIdentifier { case "MARK_AS_READ": handleMarkAsRead(userInfo: userInfo) @@ -551,11 +551,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } completionHandler() } - + // MARK: Add Feed @MainActor func addWebFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) { createAndShowMainWindowIfNecessary() - + if mainWindowController!.isDisplayingSheet { return } @@ -608,7 +608,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @MainActor @IBAction func showKeyboardShortcutsWindow(_ sender: Any?) { if keyboardShortcutsWindowController == nil { - + keyboardShortcutsWindowController = WebViewWindowController(title: NSLocalizedString("Keyboard Shortcuts", comment: "window title")) let htmlFile = Bundle(for: type(of: self)).path(forResource: "KeyboardShortcuts", ofType: "html")! keyboardShortcutsWindowController?.displayContents(of: htmlFile) @@ -619,7 +619,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let minSize = NSSize(width: 400, height: 400) window.setPointAndSizeAdjustingForScreen(point: point, size: size, minimumSize: minSize) } - + } keyboardShortcutsWindowController!.showWindow(self) @@ -644,11 +644,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, if mainWindowController!.isDisplayingSheet { return } - + importOPMLController = ImportOPMLWindowController() importOPMLController?.runSheetOnWindow(mainWindowController!.window!) } - + @MainActor @IBAction func importNNW3FromFile(_ sender: Any?) { createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { @@ -656,17 +656,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } NNW3ImportController.askUserToImportNNW3Subscriptions(window: mainWindowController!.window!) } - + @MainActor @IBAction func exportOPML(_ sender: Any?) { createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { return } - + exportOPMLController = ExportOPMLWindowController() exportOPMLController?.runSheetOnWindow(mainWindowController!.window!) } - + @MainActor @IBAction func addAppNews(_ sender: Any?) { if AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() { return @@ -678,7 +678,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, Browser.open("https://netnewswire.com/", inBackground: false) } - + @MainActor @IBAction func showHelp(_ sender: Any?) { Browser.open("https://netnewswire.com/help/mac/6.1/en/", inBackground: false) @@ -711,7 +711,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, AppDefaults.shared.timelineSortDirection = .orderedDescending } - + @MainActor @IBAction func groupByFeedToggled(_ sender: NSMenuItem) { AppDefaults.shared.timelineGroupByFeed.toggle() } @@ -721,7 +721,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, self.softwareUpdater.checkForUpdates() #endif } - + @MainActor @IBAction func showAbout(_ sender: Any?) { if #available(macOS 12, *) { for window in NSApplication.shared.windows { @@ -745,18 +745,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, public func menuNeedsUpdate(_ menu: NSMenu) { let newShareMenu = mainWindowController?.shareMenu - + guard menu != fileMenuItem.submenu else { shareMenuItem.isEnabled = newShareMenu != nil return } - + menu.removeAllItems() if let newShareMenu = newShareMenu { menu.takeItems(from: newShareMenu) } } - + } // MARK: - Debug Menu @@ -779,7 +779,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, comment: "Clear and restart confirmation message.") alert.addButton(withTitle: NSLocalizedString("Clear & Restart", comment: "Clear & Restart")) alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel")) - + let userChoice = alert.runModal() if userChoice == .alertFirstButtonReturn { CacheCleaner.purge() @@ -787,7 +787,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let configuration = NSWorkspace.OpenConfiguration() configuration.createsNewApplicationInstance = true NSWorkspace.shared.openApplication(at: Bundle.main.bundleURL, configuration: configuration) - + NSApp.terminate(self) } } @@ -840,7 +840,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, refreshTimer?.fireOldTimer() syncTimer?.fireOldTimer() } - + func objectsForInspector() -> [Any]? { guard let window = NSApplication.shared.mainWindow, let windowController = window.windowController as? MainWindowController else { return nil @@ -858,15 +858,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, sortByNewestArticleOnTopMenuItem.state = sortByNewestOnTop ? .on : .off sortByOldestArticleOnTopMenuItem.state = sortByNewestOnTop ? .off : .on } - + func updateGroupByFeedMenuItem() { let groupByFeedEnabled = AppDefaults.shared.timelineGroupByFeed groupArticlesByFeedMenuItem.state = groupByFeedEnabled ? .on : .off } - + func importTheme(filename: String) { guard let window = mainWindowController?.window else { return } - + do { let theme = try ArticleTheme(path: filename, isAppTheme: false) let alert = NSAlert() @@ -874,11 +874,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let localizedMessageText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text") alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name, theme.creatorName) as String - + var attrs = [NSAttributedString.Key : Any]() attrs[.font] = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) attrs[.foregroundColor] = NSColor.textColor - + let titleParagraphStyle = NSMutableParagraphStyle() titleParagraphStyle.alignment = .center attrs[.paragraphStyle] = titleParagraphStyle @@ -899,10 +899,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, textView.drawsBackground = false textView.textStorage?.setAttributedString(websiteText) alert.accessoryView = textView - + alert.addButton(withTitle: NSLocalizedString("Install Theme", comment: "Install Theme")) alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme")) - + func importTheme() { do { try ArticleThemesManager.shared.importTheme(filename: filename) @@ -912,7 +912,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, logger.error("Error importing theme: \(error.localizedDescription, privacy: .public)") } } - + alert.beginSheetModal(for: window) { result in if result == NSApplication.ModalResponse.alertFirstButtonReturn { @@ -925,7 +925,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, alert.addButton(withTitle: NSLocalizedString("Overwrite", comment: "Overwrite")) alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme")) - + alert.beginSheetModal(for: window) { result in if result == NSApplication.ModalResponse.alertFirstButtonReturn { importTheme() @@ -940,25 +940,25 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, presentThemeImportError(error) } } - + func confirmImportSuccess(themeName: String) { guard let window = mainWindowController?.window else { return } - + let alert = NSAlert() alert.alertStyle = .informational alert.messageText = NSLocalizedString("Theme installed", comment: "Theme installed") - + let localizedInformativeText = NSLocalizedString("The theme “%@” has been installed.", comment: "Theme installed") alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, themeName) as String - + alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) alert.beginSheetModal(for: window) } - + private func presentTwitterDeprecationAlertIfRequired() { if AppDefaults.shared.twitterDeprecationAlertShown { return } - + let expiryDate = Date(timeIntervalSince1970: 1691539200) // August 9th 2023, 00:00 UTC let currentDate = Date() if currentDate > expiryDate { @@ -970,7 +970,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } AppDefaults.shared.twitterDeprecationAlertShown = true } - + private func showTwitterDeprecationAlert() { DispatchQueue.main.async { let alert = NSAlert() @@ -983,12 +983,48 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } + private func presentTwitterDeprecationAlertIfRequired() { + if AppDefaults.shared.twitterDeprecationAlertShown { return } + + let expiryDate = Date(timeIntervalSince1970: 1691539200) // August 9th 2023, 00:00 UTC + let currentDate = Date() + if currentDate > expiryDate { + return // If after August 9th, don't show + } + + if AccountManager.shared.anyLocalOriCloudAccountHasAtLeastOneTwitterFeed() { + showTwitterDeprecationAlert() + } + AppDefaults.shared.twitterDeprecationAlertShown = true + } + + private func showTwitterDeprecationAlert() { + DispatchQueue.main.async { + let alert = NSAlert() + alert.alertStyle = .warning + alert.messageText = NSLocalizedString("Twitter Integration Removed", comment: "Twitter Integration Removed") + alert.informativeText = NSLocalizedString("Twitter has ended free access to the parts of the Twitter API that we need.\n\nSince Twitter does not provide RSS feeds, we’ve had to use the Twitter API. Without free access to that API, we can’t read feeds from Twitter.\n\nWe’ve left your Twitter feeds intact. If you have any starred items from those feeds, they will remain as long as you don’t delete those feeds.\n\nYou can still read whatever you have already downloaded. However, those feeds will no longer update.", comment: "Twitter deprecation informative text.") + alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) + alert.buttons[0].keyEquivalent = "\r" + alert.runModal() + } + } + + @objc func openThemesFolder(_ sender: Any) { + if themeImportPath == nil { + let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath) + NSWorkspace.shared.open(url) + } else { + let url = URL(fileURLWithPath: themeImportPath!) + NSWorkspace.shared.open(url.deletingLastPathComponent()) + } + } } /* the ScriptingAppDelegate protocol exposes a narrow set of accessors with internal visibility which are very similar to some private vars. - + These would be unnecessary if the similar accessors were marked internal rather than private, but for now, we'll keep the stratification of visibility */ @@ -1008,7 +1044,7 @@ extension AppDelegate : ScriptingAppDelegate { } @MainActor extension AppDelegate: NSWindowRestoration { - + @objc static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier, state: NSCoder, completionHandler: @escaping (NSWindow?, Error?) -> Void) { var mainWindow: NSWindow? = nil if identifier.rawValue == WindowRestorationIdentifiers.mainWindow { @@ -1016,39 +1052,39 @@ extension AppDelegate : ScriptingAppDelegate { } completionHandler(mainWindow, nil) } - + } // Handle Notification Actions @MainActor private extension AppDelegate { - + func handleMarkAsRead(userInfo: [AnyHashable: Any]) { markArticle(userInfo: userInfo, statusKey: .read) } - + func handleMarkAsStarred(userInfo: [AnyHashable: Any]) { markArticle(userInfo: userInfo, statusKey: .starred) } - + func markArticle(userInfo: [AnyHashable: Any], statusKey: ArticleStatus.Key) { guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { return } - + guard let account = AccountManager.shared.existingAccount(with: accountID) else { logger.debug("No account found from notification.") return } - + guard let articles = try? account.fetchArticles(.articleIDs([articleID])), !articles.isEmpty else { logger.debug("No article found from search using: \(articleID, privacy: .public)") return } - + account.mark(articles: articles, statusKey: statusKey, flag: true) { _ in } } - + } diff --git a/Shared/Importers/DefaultFeeds.opml b/Shared/Importers/DefaultFeeds.opml index ecac3d91e..b33778b47 100644 --- a/Shared/Importers/DefaultFeeds.opml +++ b/Shared/Importers/DefaultFeeds.opml @@ -6,7 +6,7 @@ - + diff --git a/Technotes/ReleaseNotes-Mac.markdown b/Technotes/ReleaseNotes-Mac.markdown index c4c935dca..ea124531e 100644 --- a/Technotes/ReleaseNotes-Mac.markdown +++ b/Technotes/ReleaseNotes-Mac.markdown @@ -1,8 +1,29 @@ # Mac Release Notes +## 6.1.2 build 6114 8 Apr 2023 + +Update default feeds to remove feeds that don’t appear to be active anymore (sadly!). + +## 6.1.1 build 6112 13 Mar 2023 + +Revised Twitter removal warning to not mention any specific month. We’re holding this release until Twitter shuts down free access to its API. + +## 6.1.1 build 6111 9 Feb 2023 + +Same as 6.1.1b4 but with updated build and version number. + +### 6.1.1b4 build 6110 9 Feb 2023 + +Update the Twitter removal warning to say “later in February” instead of “February 9,” since Twitter postponed the removal date to the 13th and might postpone it further. + +### 6.1.1b3 build 6109 6 Feb 2023 + +Update Safari extension icon (credit to Louie Mantia for the new icon) + ### 6.1.1b2 build 6108 5 Feb 2023 Remove Twitter integration. On first launch, for people with Twitter feeds, display an alert explaining what happened + Fix a crashing bug that could happen in the sidebar ### 6.1.1b1 build 6107 3 Nov 2022 diff --git a/iOS/Settings/AboutViewController.swift b/iOS/Settings/AboutViewController.swift new file mode 100644 index 000000000..d072aafca --- /dev/null +++ b/iOS/Settings/AboutViewController.swift @@ -0,0 +1,59 @@ +// +// AboutViewController.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 4/25/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +class AboutViewController: UITableViewController { + + @IBOutlet weak var aboutTextView: UITextView! + @IBOutlet weak var creditsTextView: UITextView! + @IBOutlet weak var acknowledgmentsTextView: UITextView! + @IBOutlet weak var thanksTextView: UITextView! + @IBOutlet weak var dedicationTextView: UITextView! + + override func viewDidLoad() { + + super.viewDidLoad() + + configureCell(file: "About", textView: aboutTextView) + configureCell(file: "Credits", textView: creditsTextView) + configureCell(file: "Thanks", textView: thanksTextView) + configureCell(file: "Dedication", textView: dedicationTextView) + + let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0)) + buildLabel.font = UIFont.systemFont(ofSize: 11.0) + buildLabel.textColor = UIColor.gray + buildLabel.text = NSLocalizedString("Copyright © 2002-2023 Brent Simmons", comment: "Copyright") + buildLabel.numberOfLines = 0 + buildLabel.sizeToFit() + buildLabel.translatesAutoresizingMaskIntoConstraints = false + + let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0)) + wrapperView.translatesAutoresizingMaskIntoConstraints = false + wrapperView.addSubview(buildLabel) + tableView.tableFooterView = wrapperView + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } + +} + +private extension AboutViewController { + + func configureCell(file: String, textView: UITextView) { + let url = Bundle.main.url(forResource: file, withExtension: "rtf")! + let string = try! NSAttributedString(url: url, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil) + textView.attributedText = string + textView.textColor = UIColor.label + textView.adjustsFontForContentSizeCategory = true + textView.font = .preferredFont(forTextStyle: .body) + } + +} diff --git a/xcconfig/common/NetNewsWire_mac_target_common.xcconfig b/xcconfig/common/NetNewsWire_mac_target_common.xcconfig index eb4b5b3a7..94e967287 100644 --- a/xcconfig/common/NetNewsWire_mac_target_common.xcconfig +++ b/xcconfig/common/NetNewsWire_mac_target_common.xcconfig @@ -1,6 +1,6 @@ // High Level Settings common to both the Mac application and any extensions we bundle with it -MARKETING_VERSION = 6.1.1b2 -CURRENT_PROJECT_VERSION = 6108 +MARKETING_VERSION = 6.2b1 +CURRENT_PROJECT_VERSION = 6115 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;