diff --git a/Account/Sources/Account/Feedly/Operations/FeedlyLogoutOperation.swift b/Account/Sources/Account/Feedly/Operations/FeedlyLogoutOperation.swift index 29fb7eed6..6f412eafe 100644 --- a/Account/Sources/Account/Feedly/Operations/FeedlyLogoutOperation.swift +++ b/Account/Sources/Account/Feedly/Operations/FeedlyLogoutOperation.swift @@ -34,7 +34,7 @@ final class FeedlyLogoutOperation: FeedlyOperation { assert(Thread.isMainThread) switch result { case .success: - os_log("Logged out of %{public}@ account.", "\(account.type)") + os_log("Logged out of %{public}@ account.", log: log, "\(account.type)") do { try account.removeCredentials(type: .oauthAccessToken) try account.removeCredentials(type: .oauthRefreshToken) @@ -44,7 +44,7 @@ final class FeedlyLogoutOperation: FeedlyOperation { didFinish() case .failure(let error): - os_log("Logout failed because %{public}@.", error as NSError) + os_log("Logout failed because %{public}@.", log: log, error as NSError) didFinish(with: error) } } diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 2d28c3bb7..44c877446 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -37,6 +37,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, static let mainWindow = "mainWindow" } + var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") + var userNotificationManager: UserNotificationManager! var faviconDownloader: FaviconDownloader! var imageDownloader: ImageDownloader! @@ -199,7 +201,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, AppDefaults.shared.registerDefaults() let isFirstRun = AppDefaults.shared.isFirstRun if isFirstRun { - os_log(.debug, "Is first run.") + os_log(.debug, log: log, "Is first run.") } let localAccount = AccountManager.shared.defaultAccount @@ -1010,12 +1012,12 @@ private extension AppDelegate { let account = AccountManager.shared.existingAccount(with: accountID) guard account != nil else { - os_log(.debug, "No account found from notification.") + os_log(.debug, log: log, "No account found from notification.") return } let article = try? account!.fetchArticles(.articleIDs([articleID])) guard article != nil else { - os_log(.debug, "No article found from search using %@", articleID) + os_log(.debug, log: log, "No article found from search using %@", articleID) return } account!.markArticles(article!, statusKey: .read, flag: true) { _ in } @@ -1029,12 +1031,12 @@ private extension AppDelegate { } let account = AccountManager.shared.existingAccount(with: accountID) guard account != nil else { - os_log(.debug, "No account found from notification.") + os_log(.debug, log: log, "No account found from notification.") return } let article = try? account!.fetchArticles(.articleIDs([articleID])) guard article != nil else { - os_log(.debug, "No article found from search using %@", articleID) + os_log(.debug, log: log, "No article found from search using %@", articleID) return } account!.markArticles(article!, statusKey: .starred, flag: true) { _ in } diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index e3fdc0895..294918613 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -940,7 +940,7 @@ extension TimelineViewController: NSTableViewDelegate { return [action] @unknown default: - os_log(.error, "Unknown table row edge: %ld", edge.rawValue) + print("Unknown table row edge: \(edge.rawValue)") } return [] diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index dc8075713..f107850d7 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -51,8 +51,8 @@ "repositoryURL": "https://github.com/microsoft/plcrashreporter.git", "state": { "branch": null, - "revision": "d747ab5de269cd44022bbe96ff9609d8626694ab", - "version": "1.9.0" + "revision": "6b27393cad517c067dceea85fadf050e70c4ceaa", + "version": "1.10.1" } }, { @@ -105,8 +105,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/Sparkle-Binary.git", "state": { "branch": null, - "revision": "67cd26321bdf4e77954cf6de7d9e6a20544f2030", - "version": "2.0.0" + "revision": "d1a8b3c98d96c601453f2e4230f1dd65b60d0581", + "version": "2.0.1" } }, { diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index d5808dcc5..4cf2dca03 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -58,6 +58,10 @@ struct AppAssets { static var appBadgeImage: UIImage = { return UIImage(systemName: "app.badge")! }() + + static var articleAppearanceImage: UIImage = { + return UIImage(systemName: "textformat.size")! + }() static var articleExtractorError: UIImage = { return UIImage(named: "articleExtractorError")! diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index f30a509a7..298faba77 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -408,11 +408,11 @@ private extension AppDelegate { // set expiration handler task.expirationHandler = { [weak task] in - DispatchQueue.main.sync { - self.suspendApplication() - } os_log("Accounts refresh processing terminated for running too long.", log: self.log, type: .info) - task?.setTaskCompleted(success: false) + DispatchQueue.main.async { + self.suspendApplication() + task?.setTaskCompleted(success: false) + } } } @@ -431,12 +431,12 @@ private extension AppDelegate { resumeDatabaseProcessingIfNecessary() let account = AccountManager.shared.existingAccount(with: accountID) guard account != nil else { - os_log(.debug, "No account found from notification.") + os_log(.debug, log: self.log, "No account found from notification.") return } let article = try? account!.fetchArticles(.articleIDs([articleID])) guard article != nil else { - os_log(.debug, "No article found from search using %@", articleID) + os_log(.debug, log: self.log, "No article found from search using %@", articleID) return } account!.markArticles(article!, statusKey: .read, flag: true) { _ in } @@ -459,12 +459,12 @@ private extension AppDelegate { resumeDatabaseProcessingIfNecessary() let account = AccountManager.shared.existingAccount(with: accountID) guard account != nil else { - os_log(.debug, "No account found from notification.") + os_log(.debug, log: self.log, "No account found from notification.") return } let article = try? account!.fetchArticles(.articleIDs([articleID])) guard article != nil else { - os_log(.debug, "No article found from search using %@", articleID) + os_log(.debug, log: self.log, "No article found from search using %@", articleID) return } account!.markArticles(article!, statusKey: .starred, flag: true) { _ in } diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 1da544502..9a989cb14 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -162,7 +162,6 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { searchBar.delegate = self view.bringSubviewToFront(searchBar) - configureAppearanceMenu() updateUI() } @@ -234,12 +233,20 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { starBarButtonItem.accLabelText = NSLocalizedString("Star Article", comment: "Star Article") } + configureAppearanceMenu() + configureArticleExtractorMenu() + } override func contentScrollView(for edge: NSDirectionalRectEdge) -> UIScrollView? { return currentWebViewController?.webView?.scrollView } + + /// The appearance menu is different on iPhone and iPad. + /// On iPad, it's only the theme selector. On iPhone, the appearance menu + /// contains the the theme selector and full screen options. + /// - Parameter sender: `Any?` @objc func configureAppearanceMenu(_ sender: Any? = nil) { @@ -266,6 +273,12 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { let themeMenu = UIMenu(title: "Theme", image: AppAssets.themeImage, identifier: nil, options: .singleSelection, children: [ defaultThemeMenu, customThemeMenu]) + if UIDevice.current.userInterfaceIdiom == .pad { + appearanceBarButtonItem.image = AppAssets.themeImage + appearanceBarButtonItem.menu = themeMenu + return + } + var appearanceChildren: [UIMenuElement] = [themeMenu] if let currentWebViewController = currentWebViewController { @@ -291,8 +304,15 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { } } - var feedManagementChildren = [UIMenuElement]() + let appearanceMenu = UIMenu(title: NSLocalizedString("Article Appearance", comment: "Appearance"), image: nil, identifier: nil, options: .displayInline, children: appearanceChildren) + let menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: [appearanceMenu]) + + appearanceBarButtonItem.image = AppAssets.articleAppearanceImage + appearanceBarButtonItem.menu = menu + } + + private func configureArticleExtractorMenu() { if let feed = article?.webFeed { let extractorOn = feed.isArticleExtractorAlwaysOn ?? false let readerAction = UIAction(title: NSLocalizedString("Always Use Reader View", comment: "Always Use Reader View"), @@ -303,37 +323,16 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { state: extractorOn ? .on : .off) { [weak self] _ in if feed.isArticleExtractorAlwaysOn == nil { feed.isArticleExtractorAlwaysOn = true + self?.currentWebViewController?.toggleArticleExtractor() } else { feed.isArticleExtractorAlwaysOn?.toggle() } - self?.configureAppearanceMenu() + self?.configureArticleExtractorMenu() } - feedManagementChildren.append(readerAction) - - let notifyOn = feed.isNotifyAboutNewArticles ?? false - let notifyAction = UIAction(title: NSLocalizedString("Notify About New Articles", comment: "Notify About New Articles"), - image: AppAssets.appBadgeImage, - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: notifyOn ? .on : .off) { [weak self] _ in - if feed.isNotifyAboutNewArticles == nil { - feed.isNotifyAboutNewArticles = true - } else { - feed.isNotifyAboutNewArticles?.toggle() - } - self?.configureAppearanceMenu() - } - feedManagementChildren.append(notifyAction) + let menu = UIMenu(title: feed.nameForDisplay, image: AppAssets.articleExtractorOffSF, identifier: nil, options: .displayInline, children: [readerAction]) + articleExtractorButton.menu = menu + articleExtractorButton.showsMenuAsPrimaryAction = false } - - let appearanceMenu = UIMenu(title: NSLocalizedString("Article Appearance", comment: "Appearance"), image: nil, identifier: nil, options: .displayInline, children: appearanceChildren) - let feedMgmtMenu = UIMenu(title: NSLocalizedString("Feed Management", comment: "Feed Management"), image: nil , identifier: nil, options: .displayInline, children: feedManagementChildren) - - let menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: [appearanceMenu, feedMgmtMenu]) - - appearanceBarButtonItem.menu = menu - } @@ -416,6 +415,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { @IBAction func toggleArticleExtractor(_ sender: Any) { currentWebViewController?.toggleArticleExtractor() + configureArticleExtractorMenu() } @IBAction func nextUnread(_ sender: Any) { diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 3b8faf264..91d0a2341 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -563,6 +563,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma } } + if let rowChanges = changes.rowChanges { + for rowChange in rowChanges { + if let reloads = rowChange.reloadIndexPaths, !reloads.isEmpty { + tableView.reloadRows(at: reloads, with: .none) + } + } + } + completion?() } diff --git a/iOS/MasterFeed/ShadowTableChanges.swift b/iOS/MasterFeed/ShadowTableChanges.swift index dd8a12801..49c4a9f9f 100644 --- a/iOS/MasterFeed/ShadowTableChanges.swift +++ b/iOS/MasterFeed/ShadowTableChanges.swift @@ -25,6 +25,7 @@ struct ShadowTableChanges { var section: Int var deletes: Set? var inserts: Set? + var reloads: Set? var moves: Set? var isEmpty: Bool { @@ -41,15 +42,21 @@ struct ShadowTableChanges { return inserts.map { IndexPath(row: $0, section: section) } } + var reloadIndexPaths: [IndexPath]? { + guard let reloads = reloads else { return nil } + return reloads.map { IndexPath(row: $0, section: section) } + } + var moveIndexPaths: [(IndexPath, IndexPath)]? { guard let moves = moves else { return nil } return moves.map { (IndexPath(row: $0.from, section: section), IndexPath(row: $0.to, section: section)) } } - init(section: Int, deletes: Set?, inserts: Set?, moves: Set?) { + init(section: Int, deletes: Set?, inserts: Set?, reloads: Set?, moves: Set?) { self.section = section self.deletes = deletes self.inserts = inserts + self.reloads = reloads self.moves = moves } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 2180a11ea..da737f550 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -73,8 +73,16 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { private var fetchSerialNumber = 0 private let fetchRequestQueue = FetchRequestQueue() + // Which Containers are expanded private var expandedTable = Set() + + // Which Containers used to be expanded. Reset by rebuilding the Shadow Table. + private var lastExpandedTable = Set() + + // Which Feeds have the Read Articles Filter enabled private var readFilterEnabledTable = [FeedIdentifier: Bool]() + + // Flattened tree structure for the Sidebar private var shadowTable = [(sectionID: String, feedNodes: [FeedNode])]() private(set) var preSearchTimelineFeed: Feed? @@ -675,8 +683,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { rebuildBackingStores() } + /// This is a special function that expects the caller to change the disclosure arrow state outside this function. + /// Failure to do so will get the Sidebar into an invalid state. func expand(_ node: Node) { guard let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else { return } + lastExpandedTable.insert(containerID) expand(containerID) } @@ -698,8 +709,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { clearTimelineIfNoLongerAvailable() } + /// This is a special function that expects the caller to change the disclosure arrow state outside this function. + /// Failure to do so will get the Sidebar into an invalid state. func collapse(_ node: Node) { guard let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else { return } + lastExpandedTable.remove(containerID) collapse(containerID) } @@ -1080,7 +1094,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { rebuildBackingStores(initialLoad: initialLoad, completion: { self.treeControllerDelegate.resetFilterExceptions() - self.selectFeed(webFeed, animations: animations, completion: completion) + self.selectFeed(nil) { + self.selectFeed(webFeed, animations: animations, completion: completion) + } }) } @@ -1383,6 +1399,7 @@ private extension SceneCoordinator { let toolbarAppearance = UIToolbarAppearance() navController.toolbar.standardAppearance = toolbarAppearance navController.toolbar.compactAppearance = toolbarAppearance + navController.toolbar.scrollEdgeAppearance = toolbarAppearance navController.toolbar.tintColor = AppAssets.primaryAccentColor } @@ -1512,9 +1529,10 @@ private extension SceneCoordinator { currentFeedIndexPath = indexPathFor(timelineFeed as AnyObject) } - // Compute the differences in the shadow table rows + // Compute the differences in the shadow table rows and the expanded table entries var changes = [ShadowTableChanges.RowChanges]() - + let expandedTableDifference = lastExpandedTable.symmetricDifference(expandedTable) + for (section, newSectionRows) in newShadowTable.enumerated() { var moves = Set() var inserts = Set() @@ -1540,9 +1558,22 @@ private extension SceneCoordinator { } } - changes.append(ShadowTableChanges.RowChanges(section: section, deletes: deletes, inserts: inserts, moves: moves)) + // We need to reload the difference in expanded rows to get the disclosure arrows correct when programmatically changing their state + var reloads = Set() + + for (index, newFeedNode) in newSectionRows.feedNodes.enumerated() { + if let newFeedNodeContainerID = (newFeedNode.node.representedObject as? Container)?.containerID { + if expandedTableDifference.contains(newFeedNodeContainerID) { + reloads.insert(index) + } + } + } + + changes.append(ShadowTableChanges.RowChanges(section: section, deletes: deletes, inserts: inserts, reloads: reloads, moves: moves)) } + lastExpandedTable = expandedTable + // Compute the difference in the shadow table sections var moves = Set() var inserts = Set()