Merge branch 'mac-release' into main.

This commit is contained in:
Brent Simmons
2023-04-30 17:22:47 -07:00
6 changed files with 280 additions and 164 deletions

View File

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

View File

@@ -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 couldnt 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, weve had to use the Twitter API. Without free access to that API, we cant read feeds from Twitter.\n\nWeve left your Twitter feeds intact. If you have any starred items from those feeds, they will remain as long as you dont 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 }
}
}

View File

@@ -6,7 +6,7 @@
<body>
<outline text="BBC News - World" title="BBC News - World" type="rss" version="RSS" htmlUrl="https://www.bbc.com/news" xmlUrl="https://feeds.bbci.co.uk/news/world/rss.xml"/>
<outline text="Allen Pike" title="Allen Pike" type="rss" version="RSS" htmlUrl="https://www.allenpike.com/" xmlUrl="https://feeds.allenpike.com/feed/"/>
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="https://beckyhansmeyer.com" xmlUrl="https://beckyhansmeyer.com/feed/"/>
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="https://www.beckyhansmeyer.com/" xmlUrl="https://www.beckyhansmeyer.com/feed/"/>
<outline text="Colossal" title="Colossal" type="rss" version="RSS" htmlUrl="https://www.thisiscolossal.com/" xmlUrl="https://www.thisiscolossal.com/feed/"/>
<outline text="Craig Hockenberry" title="Craig Hockenberry" type="rss" version="RSS" htmlUrl="https://furbo.org/" xmlUrl="https://furbo.org/feed/json"/>
<outline text="Daring Fireball" title="Daring Fireball" type="rss" version="RSS" htmlUrl="https://daringfireball.net/" xmlUrl="https://daringfireball.net/feeds/json"/>

View File

@@ -1,8 +1,29 @@
# Mac Release Notes
## 6.1.2 build 6114 8 Apr 2023
Update default feeds to remove feeds that dont appear to be active anymore (sadly!).
## 6.1.1 build 6112 13 Mar 2023
Revised Twitter removal warning to not mention any specific month. Were 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

View File

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

View File

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