diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index b3f7ccad8..2453f9a7e 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -653,6 +653,22 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, fetchUnreadCounts(for: webFeeds, completion: completion) } + public func fetchUnreadArticlesBetween(limit: Int?, before: Date?, after: Date?) throws -> Set
{ + return try fetchUnreadArticlesBetween(forContainer: self, limit: limit, before: before, after: after) + } + + public func fetchUnreadArticlesBetween(folder: Folder, limit: Int?, before: Date?, after: Date?) throws -> Set
{ + return try fetchUnreadArticlesBetween(forContainer: folder, limit: limit, before: before, after: after) + } + + public func fetchUnreadArticlesBetween(webFeeds: Set, limit: Int?, before: Date?, after: Date?) throws -> Set
{ + return try fetchUnreadArticlesBetween(feeds: webFeeds, limit: limit, before: before, after: after) + } + + public func fetchArticlesBetween(articleIDs: Set, before: Date?, after: Date?) throws -> Set
{ + return try database.fetchArticlesBetween(articleIDs: articleIDs, before: before, after: after) + } + public func fetchArticles(_ fetchType: FetchType) throws -> Set
{ switch fetchType { case .starred(let limit): @@ -1150,6 +1166,17 @@ private extension Account { return articles } + func fetchUnreadArticlesBetween(forContainer container: Container, limit: Int?, before: Date?, after: Date?) throws -> Set
{ + let feeds = container.flattenedWebFeeds() + let articles = try database.fetchUnreadArticlesBetween(feeds.webFeedIDs(), limit, before, after) + return articles + } + + func fetchUnreadArticlesBetween(feeds: Set, limit: Int?, before: Date?, after: Date?) throws -> Set
{ + let articles = try database.fetchUnreadArticlesBetween(feeds.webFeedIDs(), limit, before, after) + return articles + } + func fetchUnreadArticlesAsync(forContainer container: Container, limit: Int?, _ completion: @escaping ArticleSetResultBlock) { let webFeeds = container.flattenedWebFeeds() database.fetchUnreadArticlesAsync(webFeeds.webFeedIDs(), limit) { [weak self] (articleSetResult) in diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index fbf5fa32c..fa5eb2b3b 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -368,6 +368,16 @@ public final class AccountManager: UnreadCountProvider { } } } + + public func fetchUnreadArticlesBetween(limit: Int? = nil, before: Date? = nil, after: Date? = nil) throws -> Set
{ + precondition(Thread.isMainThread) + + var articles = Set
() + for account in activeAccounts { + articles.formUnion(try account.fetchUnreadArticlesBetween(limit: limit, before: before, after: after)) + } + return articles + } // MARK: - Fetching Article Counts diff --git a/Account/Sources/Account/ArticleFetcher.swift b/Account/Sources/Account/ArticleFetcher.swift index 308cb155e..53b41d810 100644 --- a/Account/Sources/Account/ArticleFetcher.swift +++ b/Account/Sources/Account/ArticleFetcher.swift @@ -15,6 +15,7 @@ public protocol ArticleFetcher { func fetchArticles() throws -> Set
func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) func fetchUnreadArticles() throws -> Set
+ func fetchUnreadArticlesBetween(before: Date?, after: Date?) throws -> Set
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) } @@ -37,6 +38,10 @@ extension WebFeed: ArticleFetcher { return try fetchArticles().unreadArticles() } + public func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + return try account?.fetchUnreadArticlesBetween(webFeeds: [self], limit: nil, before: before, after: after) ?? Set
() + } + public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { guard let account = account else { assertionFailure("Expected feed.account, but got nil.") @@ -81,6 +86,14 @@ extension Folder: ArticleFetcher { return try account.fetchArticles(.folder(self, true)) } + public func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + guard let account = account else { + assertionFailure("Expected folder.account, but got nil.") + return Set
() + } + return try account.fetchUnreadArticlesBetween(folder: self, limit: nil, before: before, after: after) + } + public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { guard let account = account else { assertionFailure("Expected folder.account, but got nil.") diff --git a/Account/Sources/Account/SingleArticleFetcher.swift b/Account/Sources/Account/SingleArticleFetcher.swift index f558fd5fa..ab3e26e3b 100644 --- a/Account/Sources/Account/SingleArticleFetcher.swift +++ b/Account/Sources/Account/SingleArticleFetcher.swift @@ -31,6 +31,10 @@ public struct SingleArticleFetcher: ArticleFetcher { public func fetchUnreadArticles() throws -> Set
{ return try account.fetchArticles(.articleIDs(Set([articleID]))) } + + public func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + return try account.fetchArticlesBetween(articleIDs: Set([articleID]), before: before, after: after) + } public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion) diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift index e32a83198..33084286d 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift @@ -106,6 +106,14 @@ public final class ArticlesDatabase { return try articlesTable.fetchUnreadArticles(webFeedIDs, limit) } + public func fetchArticlesBetween(articleIDs: Set, before: Date?, after: Date?) throws -> Set
{ + return try articlesTable.fetchArticlesBetween(articleIDs: articleIDs, before: before, after: after) + } + + public func fetchUnreadArticlesBetween(_ webFeedIDs: Set, _ limit: Int?, _ before: Date?, _ after: Date?) throws -> Set
{ + return try articlesTable.fetchUnreadArticlesBetween(webFeedIDs, limit, before, after) + } + public func fetchTodayArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate(), limit) } diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift index 94d75d94a..ac59619da 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift @@ -70,6 +70,10 @@ final class ArticlesTable: DatabaseTable { return try fetchArticles{ self.fetchArticles(articleIDs: articleIDs, $0) } } + func fetchArticlesBetween(articleIDs: Set, before: Date?, after: Date?) throws -> Set
{ + return try fetchArticles{ self.fetchArticlesBetween(articleIDs: articleIDs, before: before, after: after, $0) } + } + func fetchArticlesAsync(articleIDs: Set, _ completion: @escaping ArticleSetResultBlock) { return fetchArticlesAsync({ self.fetchArticles(articleIDs: articleIDs, $0) }, completion) } @@ -80,6 +84,10 @@ final class ArticlesTable: DatabaseTable { return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, limit, $0) } } + func fetchUnreadArticlesBetween(_ webFeedIDs: Set, _ limit: Int?, _ before: Date?, _ after: Date?) throws -> Set
{ + return try fetchArticles{ self.fetchUnreadArticlesBetween(webFeedIDs, limit, $0, before, after) } + } + func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, limit, $0) }, completion) } @@ -845,6 +853,30 @@ private extension ArticlesTable { return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } + func fetchUnreadArticlesBetween(_ webFeedIDs: Set, _ limit: Int?, _ database: FMDatabase, _ before: Date?, _ after: Date?) -> Set
{ + // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0 + if webFeedIDs.isEmpty { + return Set
() + } + var parameters = webFeedIDs.map { $0 as AnyObject } + + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! + var whereClause = "feedID in \(placeholders) and read=0" + + if let before = before { + whereClause.append(" and (datePublished < ? or (datePublished is null and dateArrived < ?))") + parameters = parameters + [before as AnyObject, before as AnyObject] + } + if let after = after { + whereClause.append(" and (datePublished > ? or (datePublished is null and dateArrived > ?))") + parameters = parameters + [after as AnyObject, after as AnyObject] + } + if let limit = limit { + whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)") + } + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) + } + func fetchArticlesForFeedID(_ webFeedID: String, _ database: FMDatabase) -> Set
{ return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject]) } @@ -859,6 +891,26 @@ private extension ArticlesTable { return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } + func fetchArticlesBetween(articleIDs: Set, before: Date?, after: Date?, _ database: FMDatabase) -> Set
{ + if articleIDs.isEmpty { + return Set
() + } + var parameters = articleIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))! + var whereClause = "articleID in \(placeholders)" + + if let before = before { + whereClause.append(" and (datePublished < ? or (datePublished is null and dateArrived < ?))") + parameters = parameters + [before as AnyObject, before as AnyObject] + } + if let after = after { + whereClause.append(" and (datePublished > ? or (datePublished is null and dateArrived > ?))") + parameters = parameters + [after as AnyObject, after as AnyObject] + } + + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) + } + func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date, _ limit: Int?, _ database: FMDatabase) -> Set
{ // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?) // diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 92db499cf..0d2d0bae6 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -703,35 +703,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, Browser.open("https://netnewswire.com/", inBackground: false) } - @IBAction func openReleaseNotes(_ sender: Any?) { - Browser.open(URL.releaseNotes.absoluteString, inBackground: false) - } - - - @IBAction func openHowToSupport(_ sender: Any?) { - - Browser.open("https://github.com/brentsimmons/NetNewsWire/blob/main/Technotes/HowToSupportNetNewsWire.markdown", inBackground: false) - } - - @IBAction func openRepository(_ sender: Any?) { - - Browser.open("https://github.com/brentsimmons/NetNewsWire", inBackground: false) - } - - @IBAction func openBugTracker(_ sender: Any?) { - - Browser.open("https://github.com/brentsimmons/NetNewsWire/issues", inBackground: false) - } - - @IBAction func openSlackGroup(_ sender: Any?) { - Browser.open("https://netnewswire.com/slack", inBackground: false) - } - - @IBAction func openTechnotes(_ sender: Any?) { - - Browser.open("https://github.com/brentsimmons/NetNewsWire/tree/main/Technotes", inBackground: false) - } - @IBAction func showHelp(_ sender: Any?) { Browser.open("https://netnewswire.com/help/mac/6.1/en/", inBackground: false) diff --git a/Mac/Base.lproj/Main.storyboard b/Mac/Base.lproj/Main.storyboard index 06967fdb1..46b22d53b 100644 --- a/Mac/Base.lproj/Main.storyboard +++ b/Mac/Base.lproj/Main.storyboard @@ -646,48 +646,12 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift index d4ae09c91..35975c32a 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift @@ -84,6 +84,56 @@ extension SidebarViewController { runCommand(markReadCommand) } + + @objc func markObjectsReadOlderThanOneDayFromContextualMenu(_ sender: Any?) { + return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .day, value: -1, to: Date()), after: nil, sender: sender) + } + + @objc func markObjectsReadOlderThanTwoDaysFromContextualMenu(_ sender: Any?) { + return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .day, value: -2, to: Date()), after: nil, sender: sender) + } + + @objc func markObjectsReadOlderThanThreeDaysFromContextualMenu(_ sender: Any?) { + return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .day, value: -3, to: Date()), after: nil, sender: sender) + } + + @objc func markObjectsReadOlderThanOneWeekFromContextualMenu(_ sender: Any?) { + return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .weekOfYear, value: -1, to: Date()), after: nil, sender: sender) + } + + @objc func markObjectsReadOlderThanTwoWeeksFromContextualMenu(_ sender: Any?) { + return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .weekOfYear, value: -2, to: Date()), after: nil, sender: sender) + } + + @objc func markObjectsReadOlderThanOneMonthFromContextualMenu(_ sender: Any?) { + return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .month, value: -1, to: Date()), after: nil, sender: sender) + } + + @objc func markObjectsReadOlderThanOneYearFromContextualMenu(_ sender: Any?) { + return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .year, value: -1, to: Date()), after: nil, sender: sender) + } + + func markObjectsReadBetweenDatesFromContextualMenu(before: Date?, after: Date?, sender: Any?) { + + guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [Any] else { + return + } + + var markableArticles = unreadArticlesBetween(for: objects, before: before, after: after) + if let directlyMarkedAsUnreadArticles = delegate?.directlyMarkedAsUnreadArticles { + markableArticles = markableArticles.subtracting(directlyMarkedAsUnreadArticles) + } + + guard let undoManager = undoManager, + let markReadCommand = MarkStatusCommand(initialArticles: markableArticles, + markingRead: true, + directlyMarked: false, + undoManager: undoManager) else { + return + } + runCommand(markReadCommand) + } + @objc func deleteFromContextualMenu(_ sender: Any?) { guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [AnyObject] else { return @@ -220,6 +270,12 @@ private extension SidebarViewController { if webFeed.unreadCount > 0 { menu.addItem(markAllReadMenuItem([webFeed])) + let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark as Read Older Than", comment: "Command Submenu"), action: nil, keyEquivalent: "") + let catchUpSubMenu = catchUpSubMenu([webFeed]) + + menu.addItem(catchUpMenuItem) + menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem) + menu.addItem(NSMenuItem.separator()) } @@ -276,6 +332,11 @@ private extension SidebarViewController { if folder.unreadCount > 0 { menu.addItem(markAllReadMenuItem([folder])) + let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark as Read Older Than", comment: "Command Submenu"), action: nil, keyEquivalent: "") + let catchUpSubMenu = catchUpSubMenu([folder]) + + menu.addItem(catchUpMenuItem) + menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem) menu.addItem(NSMenuItem.separator()) } @@ -291,6 +352,18 @@ private extension SidebarViewController { if smartFeed.unreadCount > 0 { menu.addItem(markAllReadMenuItem([smartFeed])) + + // Doesn't make sense to mark articles newer than a day with catch up with first option being older than a day + if let maybeSmartFeed = smartFeed as? SmartFeed { + if maybeSmartFeed.delegate is TodayFeedDelegate { + return menu + } + } + + let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark as Read Older Than", comment: "Command Submenu"), action: nil, keyEquivalent: "") + let catchUpSubMenu = catchUpSubMenu([smartFeed]) + menu.addItem(catchUpMenuItem) + menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem) } return menu.numberOfItems > 0 ? menu : nil } @@ -301,6 +374,11 @@ private extension SidebarViewController { if anyObjectInArrayHasNonZeroUnreadCount(objects) { menu.addItem(markAllReadMenuItem(objects)) + let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark as Read Older Than", comment: "Command Submenu"), action: nil, keyEquivalent: "") + let catchUpSubMenu = catchUpSubMenu(objects) + + menu.addItem(catchUpMenuItem) + menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem) } if allObjectsAreFeedsAndOrFolders(objects) { @@ -316,6 +394,20 @@ private extension SidebarViewController { return menuItem(NSLocalizedString("button.title.mark-all-as-read.titlecase", comment: "Mark All as Read"), #selector(markObjectsReadFromContextualMenu(_:)), objects) } + func catchUpSubMenu(_ objects: [Any]) -> NSMenu { + let menu = NSMenu(title: "Catch up to articles older than...") + + menu.addItem(menuItem(NSLocalizedString("1 day", comment: "Command"), #selector(markObjectsReadOlderThanOneDayFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("2 days", comment: "Command"), #selector(markObjectsReadOlderThanTwoDaysFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("3 days", comment: "Command"), #selector(markObjectsReadOlderThanThreeDaysFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("1 week", comment: "Command"), #selector(markObjectsReadOlderThanOneWeekFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("2 weeks", comment: "Command"), #selector(markObjectsReadOlderThanTwoWeeksFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("1 month", comment: "Command"), #selector(markObjectsReadOlderThanOneMonthFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("1 year", comment: "Command"), #selector(markObjectsReadOlderThanOneYearFromContextualMenu(_:)), objects)) + + return menu + } + func deleteMenuItem(_ objects: [Any]) -> NSMenuItem { return menuItem(NSLocalizedString("button.title.delete", comment: "Delete"), #selector(deleteFromContextualMenu(_:)), objects) @@ -373,5 +465,18 @@ private extension SidebarViewController { } return articles } + + func unreadArticlesBetween(for objects: [Any], before: Date?, after: Date?) -> Set
{ + + var articles = Set
() + for object in objects { + if let articleFetcher = object as? ArticleFetcher { + if let unreadArticles = try? articleFetcher.fetchUnreadArticlesBetween(before: before, after: after) { + articles.formUnion(unreadArticles) + } + } + } + return articles + } } diff --git a/Shared/SmartFeeds/SearchFeedDelegate.swift b/Shared/SmartFeeds/SearchFeedDelegate.swift index 23f14ffdf..90b784d8a 100644 --- a/Shared/SmartFeeds/SearchFeedDelegate.swift +++ b/Shared/SmartFeeds/SearchFeedDelegate.swift @@ -35,5 +35,10 @@ struct SearchFeedDelegate: SmartFeedDelegate { func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock) { // TODO: after 5.0 } + + func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + fatalError("Function not implemented.") + } + } diff --git a/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift b/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift index a1ca0bf53..d8de2851a 100644 --- a/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift +++ b/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift @@ -35,4 +35,9 @@ struct SearchTimelineFeedDelegate: SmartFeedDelegate { func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock) { // TODO: after 5.0 } + + func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + fatalError("Function not implemented.") + } + } diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index e8245a0b9..aaa9a5c6a 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -46,7 +46,7 @@ final class SmartFeed: PseudoFeed { } #endif - private let delegate: SmartFeedDelegate + public let delegate: SmartFeedDelegate private var unreadCounts = [String: Int]() init(delegate: SmartFeedDelegate) { @@ -95,6 +95,10 @@ extension SmartFeed: ArticleFetcher { return try delegate.fetchUnreadArticles() } + func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + return try delegate.fetchUnreadArticlesBetween(before: before, after: after) + } + func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { delegate.fetchUnreadArticlesAsync(completion) } diff --git a/Shared/SmartFeeds/SmartFeedDelegate.swift b/Shared/SmartFeeds/SmartFeedDelegate.swift index a8bb89e96..1f9f9b1f0 100644 --- a/Shared/SmartFeeds/SmartFeedDelegate.swift +++ b/Shared/SmartFeeds/SmartFeedDelegate.swift @@ -31,6 +31,10 @@ extension SmartFeedDelegate { return try fetchArticles().unreadArticles() } + func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after) + } + func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { fetchArticlesAsync{ articleSetResult in switch articleSetResult { diff --git a/Shared/SmartFeeds/StarredFeedDelegate.swift b/Shared/SmartFeeds/StarredFeedDelegate.swift index 1eb58adec..012e7a58e 100644 --- a/Shared/SmartFeeds/StarredFeedDelegate.swift +++ b/Shared/SmartFeeds/StarredFeedDelegate.swift @@ -29,4 +29,8 @@ struct StarredFeedDelegate: SmartFeedDelegate { func fetchUnreadCount(for account: Account, completion: @escaping SingleUnreadCountCompletionBlock) { account.fetchUnreadCountForStarredArticles(completion) } + + func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after).filter({ $0.status.starred }) + } } diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index 32aeb7cce..b0fe3a44f 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -78,6 +78,10 @@ extension UnreadFeed: ArticleFetcher { return try AccountManager.shared.fetchArticles(fetchType) } + func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after) + } + func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { AccountManager.shared.fetchArticlesAsync(fetchType, completion) } diff --git a/Shared/Timer/AccountRefreshTimer.swift b/Shared/Timer/AccountRefreshTimer.swift index c60e9eade..33d07c32a 100644 --- a/Shared/Timer/AccountRefreshTimer.swift +++ b/Shared/Timer/AccountRefreshTimer.swift @@ -73,8 +73,7 @@ class AccountRefreshTimer { lastTimedRefresh = Date() update() - - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log, completion: nil) + AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) } } diff --git a/iOS/Intents/AddWebFeedIntentHandler.swift b/iOS/Intents/AddWebFeedIntentHandler.swift index aa221f562..c32c2ab10 100644 --- a/iOS/Intents/AddWebFeedIntentHandler.swift +++ b/iOS/Intents/AddWebFeedIntentHandler.swift @@ -35,16 +35,24 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling { completion(.success(with: url)) } - public func provideAccountNameOptions(for intent: AddWebFeedIntent, with completion: @escaping ([String]?, Error?) -> Void) { + public func resolveTitle(for intent: AddWebFeedIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + guard let title = intent.title else { + completion(INStringResolutionResult.notRequired()) + return + } + completion(.success(with: title)) + } + + public func provideAccountNameOptionsCollection(for intent: AddWebFeedIntent, with completion: @escaping (INObjectCollection?, Error?) -> Void) { guard let extensionContainers = ExtensionContainersFile.read() else { completion(nil, AddWebFeedIntentHandlerError.communicationFailure) return } let accountNames = extensionContainers.accounts.map { $0.name } - completion(accountNames, nil) + completion(INObjectCollection(items: accountNames as [NSString]), nil) } - + public func resolveAccountName(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedAccountNameResolutionResult) -> Void) { guard let accountName = intent.accountName else { completion(AddWebFeedAccountNameResolutionResult.notRequired()) @@ -78,6 +86,21 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling { completion(folderNames, nil) } + public func provideFolderNameOptionsCollection(for intent: AddWebFeedIntent, with completion: @escaping (INObjectCollection?, Error?) -> Void) { + guard let extensionContainers = ExtensionContainersFile.read() else { + completion(nil, AddWebFeedIntentHandlerError.communicationFailure) + return + } + + guard let accountName = intent.accountName, let account = extensionContainers.findAccount(forName: accountName) else { + completion(INObjectCollection(items: [NSString]()), nil) + return + } + + let folderNames = account.folders.map { $0.name } + completion(INObjectCollection(items: folderNames as [NSString]), nil) + } + public func resolveFolderName(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedFolderNameResolutionResult) -> Void) { guard let accountName = intent.accountName, let folderName = intent.folderName else { completion(AddWebFeedFolderNameResolutionResult.notRequired()) @@ -135,7 +158,7 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling { return } - let request = ExtensionFeedAddRequest(name: nil, feedURL: url, destinationContainerID: containerID) + let request = ExtensionFeedAddRequest(name: intent.title, feedURL: url, destinationContainerID: containerID) ExtensionFeedAddRequestFile.save(request) completion(AddWebFeedIntentResponse(code: .success, userActivity: nil)) } diff --git a/iOS/Intents/Base.lproj/Intents.intentdefinition b/iOS/Intents/Base.lproj/Intents.intentdefinition index e1969b641..6a9bdd91b 100644 --- a/iOS/Intents/Base.lproj/Intents.intentdefinition +++ b/iOS/Intents/Base.lproj/Intents.intentdefinition @@ -5,15 +5,15 @@ INEnums INIntentDefinitionModelVersion - 1.1 + 1.2 INIntentDefinitionNamespace U6u7RF INIntentDefinitionSystemVersion - 19D76 + 22A400 INIntentDefinitionToolsBuildVersion - 11B53 + 14B47b INIntentDefinitionToolsVersion - 11.2.1 + 14.1 INIntents @@ -32,21 +32,10 @@ INIntentKeyParameter url INIntentLastParameterTag - 4 + 5 INIntentManagedParameterCombinations - url,accountName - - INIntentParameterCombinationSupportsBackgroundExecution - - INIntentParameterCombinationTitle - Add ${url} to ${accountName} - INIntentParameterCombinationTitleID - kaKsEY - INIntentParameterCombinationUpdatesLinked - - - url,accountName,folderName + url,accountName,folderName,title INIntentParameterCombinationSupportsBackgroundExecution @@ -57,12 +46,25 @@ INIntentParameterCombinationUpdatesLinked + url,accountName,title + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Add ${url} to ${accountName} + INIntentParameterCombinationTitleID + kaKsEY + INIntentParameterCombinationUpdatesLinked + + INIntentName AddWebFeed INIntentParameters + INIntentParameterConfigurable + INIntentParameterDisplayName URL INIntentParameterDisplayNameID @@ -105,6 +107,52 @@ + INIntentParameterConfigurable + + INIntentParameterDisplayName + Title + INIntentParameterDisplayNameID + Ac5RHN + INIntentParameterDisplayPriority + 2 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Words + INIntentParameterMetadataDefaultValueID + SVcvQb + + INIntentParameterName + title + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + What is the ${title}of the feed? + INIntentParameterPromptDialogFormatStringID + IGNcSh + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 5 + INIntentParameterType + String + + + INIntentParameterConfigurable + INIntentParameterCustomDisambiguation INIntentParameterDisplayName @@ -112,7 +160,7 @@ INIntentParameterDisplayNameID CSrgUY INIntentParameterDisplayPriority - 2 + 3 INIntentParameterMetadata INIntentParameterMetadataCapitalization @@ -138,14 +186,6 @@ INIntentParameterPromptDialogType DisambiguationIntroduction - - INIntentParameterPromptDialogFormatString - Which one? - INIntentParameterPromptDialogFormatStringID - fWs3li - INIntentParameterPromptDialogType - DisambiguationSelection - INIntentParameterPromptDialogCustom @@ -190,6 +230,8 @@ + INIntentParameterConfigurable + INIntentParameterCustomDisambiguation INIntentParameterDisplayName @@ -197,7 +239,7 @@ INIntentParameterDisplayNameID zXhMPF INIntentParameterDisplayPriority - 3 + 4 INIntentParameterMetadata INIntentParameterMetadataCapitalization @@ -223,14 +265,6 @@ INIntentParameterPromptDialogType DisambiguationIntroduction - - INIntentParameterPromptDialogFormatString - Which one? - INIntentParameterPromptDialogFormatStringID - gEzXaM - INIntentParameterPromptDialogType - DisambiguationSelection - INIntentParameterPromptDialogCustom diff --git a/iOS/IntentsExtension/Info.plist b/iOS/IntentsExtension/Info.plist index ecb576837..e3e1b4889 100644 --- a/iOS/IntentsExtension/Info.plist +++ b/iOS/IntentsExtension/Info.plist @@ -2,8 +2,6 @@ - OrganizationIdentifier - $(ORGANIZATION_IDENTIFIER) AppGroup group.$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS AppIdentifierPrefix @@ -44,5 +42,7 @@ NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).IntentHandler + OrganizationIdentifier + $(ORGANIZATION_IDENTIFIER) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 9d5284e79..c20efa479 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -693,6 +693,9 @@ extension MasterFeedViewController: UIContextMenuInteractionDelegate { menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction])) } + if let catchUpAction = self.catchUpActionMenu(account: account, contentView: interaction.view) { + menuElements.append(catchUpAction) + } menuElements.append(UIMenu(title: "", options: .displayInline, children: [self.deactivateAccountAction(account: account)])) return UIMenu(title: "", children: menuElements) @@ -921,6 +924,11 @@ private extension MasterFeedViewController { if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) { menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction])) + + } + + if let catchUpAction = self.catchUpActionMenu(indexPath: indexPath) { + menuElements.append(catchUpAction) } if includeDeleteRename { @@ -948,6 +956,10 @@ private extension MasterFeedViewController { if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) { menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction])) } + + if let catchUpAction = self.catchUpActionMenu(indexPath: indexPath) { + menuElements.append(catchUpAction) + } menuElements.append(UIMenu(title: "", options: .displayInline, @@ -961,13 +973,22 @@ private extension MasterFeedViewController { }) } - func makePseudoFeedContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration? { - guard let markAllAction = self.markAllAsReadAction(indexPath: indexPath) else { - return nil - } + func makePseudoFeedContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration { + return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [weak self] suggestedActions in - return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { suggestedActions in - return UIMenu(title: "", children: [markAllAction]) + guard let self = self else { return nil } + + var menuElements = [UIMenuElement]() + + if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) { + menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction])) + } + + if let catchUpAction = self.catchUpActionMenu(indexPath: indexPath) { + menuElements.append(catchUpAction) + } + + return UIMenu(title: "", children: menuElements) }) } @@ -1151,6 +1172,97 @@ private extension MasterFeedViewController { return action } + func catchUpActionMenu(indexPath: IndexPath) -> UIMenu? { + guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed, + let contentView = self.tableView.cellForRow(at: indexPath)?.contentView, + feed.unreadCount > 0 else { + return nil + } + + // Doesn't make sense to mark articles newer than a day with catch up with first option being older than a day + if let maybeSmartFeed = feed as? SmartFeed { + if maybeSmartFeed.delegate is TodayFeedDelegate { + return nil + } + } + + let title = NSLocalizedString("Mark as Read Older Than", comment: "Command") + let oneDayAction = UIAction(title: "1 Day") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Day as Read", sourceType: contentView) { [weak self] in + let cutoff = Calendar.current.date(byAdding: .day, value: -1, to: Date()) + if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + let twoDayAction = UIAction(title: "2 Days") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 2 Days as Read", sourceType: contentView) { [weak self] in + let cutoff = Calendar.current.date(byAdding: .day, value: -2, to: Date()) + if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + let threeDayAction = UIAction(title: "3 Days") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 3 Days as Read", sourceType: contentView) { [weak self] in + let cutoff = Calendar.current.date(byAdding: .day, value: -3, to: Date()) + if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + let oneWeekAction = UIAction(title: "1 Week") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Week as Read", sourceType: contentView) { [weak self] in + let cutoff = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: Date()) + if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + let twoWeekAction = UIAction(title: "2 Weeks") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 2 Weeks as Read", sourceType: contentView) { [weak self] in + let cutoff = Calendar.current.date(byAdding: .weekOfYear, value: -2, to: Date()) + if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + let oneMonthAction = UIAction(title: "1 Month") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Month as Read", sourceType: contentView) { [weak self] in + let cutoff = Calendar.current.date(byAdding: .month, value: -1, to: Date()) + if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + let oneYearAction = UIAction(title: "1 Year") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Year as Read", sourceType: contentView) { [weak self] in + let cutoff = Calendar.current.date(byAdding: .year, value: -1, to: Date()) + if let articles = try? feed.fetchUnreadArticlesBetween(before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + var markActions = [UIAction]() + markActions.append(oneDayAction) + markActions.append(twoDayAction) + markActions.append(threeDayAction) + markActions.append(oneWeekAction) + markActions.append(twoWeekAction) + markActions.append(oneMonthAction) + markActions.append(oneYearAction) + let majorMenu = UIMenu(title: title, image: getMarkOlderImageDirection(), children: markActions) + + return majorMenu + } + + func getMarkOlderImageDirection() -> UIImage { + if AppDefaults.shared.timelineSortDirection == .orderedDescending { + return AppAssets.markBelowAsReadImage + } else { + return AppAssets.markAboveAsReadImage + } + } func markAllAsReadAction(account: Account, contentView: UIView?) -> UIAction? { guard account.unreadCount > 0, let contentView = contentView else { return nil @@ -1171,6 +1283,102 @@ private extension MasterFeedViewController { return action } + func catchUpActionMenu(account: Account, contentView: UIView?) -> UIMenu? { + guard account.unreadCount > 0, let contentView = contentView else { + return nil + } + + let title = NSLocalizedString("Mark as Read Older Than", comment: "Command") + let oneDayAction = UIAction(title: "1 Day") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Day as Read", sourceType: contentView) { [weak self] in + // If you don't have this delay the screen flashes when it executes this code + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let cutoff = Calendar.current.date(byAdding: .day, value: -1, to: Date()) + if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + } + let twoDayAction = UIAction(title: "2 Days") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 2 Days as Read", sourceType: contentView) { [weak self] in + // If you don't have this delay the screen flashes when it executes this code + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let cutoff = Calendar.current.date(byAdding: .day, value: -2, to: Date()) + if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + } + let threeDayAction = UIAction(title: "3 Days") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 3 Days as Read", sourceType: contentView) { [weak self] in + // If you don't have this delay the screen flashes when it executes this code + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let cutoff = Calendar.current.date(byAdding: .day, value: -3, to: Date()) + if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + } + let oneWeekAction = UIAction(title: "1 Week") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Week as Read", sourceType: contentView) { [weak self] in + // If you don't have this delay the screen flashes when it executes this code + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let cutoff = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: Date()) + if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + } + let twoWeekAction = UIAction(title: "2 Weeks") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 2 Weeks as Read", sourceType: contentView) { [weak self] in + // If you don't have this delay the screen flashes when it executes this code + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let cutoff = Calendar.current.date(byAdding: .weekOfYear, value: -2, to: Date()) + if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + } + let oneMonthAction = UIAction(title: "1 Month") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Month as Read", sourceType: contentView) { [weak self] in + // If you don't have this delay the screen flashes when it executes this code + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let cutoff = Calendar.current.date(byAdding: .month, value: -1, to: Date()) + if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + } + let oneYearAction = UIAction(title: "1 Year") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: "Mark Older Than 1 Year as Read", sourceType: contentView) { [weak self] in + // If you don't have this delay the screen flashes when it executes this code + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + let cutoff = Calendar.current.date(byAdding: .year, value: -1, to: Date()) + if let articles = try? account.fetchUnreadArticlesBetween(limit: nil, before: cutoff, after: nil) { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + } + var markActions = [UIAction]() + markActions.append(oneDayAction) + markActions.append(twoDayAction) + markActions.append(threeDayAction) + markActions.append(oneWeekAction) + markActions.append(twoWeekAction) + markActions.append(oneMonthAction) + markActions.append(oneYearAction) + let majorMenu = UIMenu(title: title, image: getMarkOlderImageDirection(), children: markActions) + + return majorMenu + } + func rename(indexPath: IndexPath) { guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return } diff --git a/iOS/Resources/Info.plist b/iOS/Resources/Info.plist index 1b350b77f..eb286c11f 100644 --- a/iOS/Resources/Info.plist +++ b/iOS/Resources/Info.plist @@ -6,14 +6,33 @@ group.$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS AppIdentifierPrefix $(AppIdentifierPrefix) - ITSAppUsesNonExemptEncryption - BGTaskSchedulerPermittedIdentifiers com.ranchero.NetNewsWire.FeedRefresh CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + nnwtheme + + CFBundleTypeName + NetNewsWire Theme + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSItemContentTypes + + com.ranchero.netnewswire.theme + + LSTypeIsPackage + + + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -54,6 +73,8 @@ $(CURRENT_PROJECT_VERSION) DeveloperEntitlements $(DEVELOPER_ENTITLEMENTS) + ITSAppUsesNonExemptEncryption + LSApplicationQueriesSchemes mailto @@ -61,6 +82,8 @@ LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + NSAppTransportSecurity NSAllowsArbitraryLoads @@ -195,29 +218,6 @@ - CFBundleDocumentTypes - - - CFBundleTypeExtensions - - nnwtheme - - CFBundleTypeName - NetNewsWire Theme - LSHandlerRank - Owner - CFBundleTypeRole - Viewer - LSItemContentTypes - - com.ranchero.netnewswire.theme - - LSTypeIsPackage - - - - LSSupportsOpeningDocumentsInPlace - UserAgent NetNewsWire (RSS Reader; https://netnewswire.com/) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index f2142aed0..0cb55c1f0 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -352,6 +352,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } } } + + if let isSidebarHidden = windowState[UserInfoKey.isSidebarHidden] as? Bool, isSidebarHidden { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.rootSplitViewController.preferredDisplayMode = .secondaryOnly + } + } rebuildBackingStores(initialLoad: true) @@ -2124,7 +2130,8 @@ private extension SceneCoordinator { return [ UserInfoKey.readFeedsFilterState: isReadFeedsFiltered, UserInfoKey.containerExpandedWindowState: containerExpandedWindowState, - UserInfoKey.readArticlesFilterState: readArticlesFilterState + UserInfoKey.readArticlesFilterState: readArticlesFilterState, + UserInfoKey.isSidebarHidden: rootSplitViewController.displayMode == .secondaryOnly ] }