From b7a358c98663b316ffc56fd6ef043b637f63d337 Mon Sep 17 00:00:00 2001 From: Bryan Culver Date: Mon, 26 Sep 2022 07:03:14 -0400 Subject: [PATCH 01/13] Prototype --- ...idebarViewController+ContextualMenus.swift | 29 ++++++++++++++++- iOS/MasterFeed/MasterFeedViewController.swift | 31 ++++++++++++++++++- .../MarkAsReadAlertController.swift | 27 ++++++++++------ 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift index 46286cd64..9ede13706 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift @@ -211,7 +211,13 @@ private extension SidebarViewController { let menu = NSMenu(title: "") if webFeed.unreadCount > 0 { - menu.addItem(markAllReadMenuItem([webFeed])) +// menu.addItem(markAllReadMenuItem([webFeed])) + let catchUpMenuItem = catchUpMenuItem([webFeed]) + let catchUpSubMenu = catchUpSubMenu([webFeed]) + + menu.addItem(catchUpMenuItem) + menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem) + menu.addItem(NSMenuItem.separator()) } @@ -307,6 +313,27 @@ private extension SidebarViewController { return menuItem(NSLocalizedString("Mark All as Read", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects) } + + func catchUpSubMenu(_ objects: [Any]) -> NSMenu { + + let menu = NSMenu(title: "Catch up to articles older than...") + + menu.addItem(menuItem(NSLocalizedString("All", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("Older than 1 day", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("Older than 2 days", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("Older than 3 days", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("Older than 1 week", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("Older than 2 weeks", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("Older than 1 month", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) + menu.addItem(menuItem(NSLocalizedString("Older than 1 year", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) + + return menu + } + + func catchUpMenuItem(_ objects: [Any]) -> NSMenuItem { + + return menuItem(NSLocalizedString("Mark as Read...", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects) + } func deleteMenuItem(_ objects: [Any]) -> NSMenuItem { diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index f8307cf20..833bf0345 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -905,8 +905,17 @@ private extension MasterFeedViewController { menuElements.append(UIMenu(title: "", options: .displayInline, children: pageActions)) } + var markActions = [UIAction]() if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) { - menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction])) + markActions.append(markAllAction) + } + + if let catchUpAction = self.catchUpAction(indexPath: indexPath) { + markActions.append(catchUpAction) + } + if !markActions.isEmpty { + menuElements.append(UIMenu(title: "", options: .displayInline, children: markActions)) + } if includeDeleteRename { @@ -1139,6 +1148,26 @@ private extension MasterFeedViewController { return action } + func catchUpAction(indexPath: IndexPath) -> UIAction? { + guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed, + let contentView = self.tableView.cellForRow(at: indexPath)?.contentView, + feed.unreadCount > 0 else { + return nil + } + + let localizedMenuText = NSLocalizedString("Catch Up in “%@”", comment: "Command") + let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String + let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in + if let articles = try? feed.fetchUnreadArticles() { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + + return action + } + func markAllAsReadAction(account: Account, contentView: UIView?) -> UIAction? { guard account.unreadCount > 0, let contentView = contentView else { return nil diff --git a/iOS/MasterTimeline/MarkAsReadAlertController.swift b/iOS/MasterTimeline/MarkAsReadAlertController.swift index 5bbce1406..fe37f2df8 100644 --- a/iOS/MasterTimeline/MarkAsReadAlertController.swift +++ b/iOS/MasterTimeline/MarkAsReadAlertController.swift @@ -46,23 +46,30 @@ struct MarkAsReadAlertController { completion: @escaping (UIAlertAction) -> Void) -> UIAlertController where T: MarkAsReadAlertControllerSourceType { - let title = NSLocalizedString("Mark As Read", comment: "Mark As Read") - let message = NSLocalizedString("You can turn this confirmation off in Settings.", - comment: "You can turn this confirmation off in Settings.") + let title = NSLocalizedString("Mark as Read", comment: "Catch Up") + let message = NSLocalizedString("Mark articles as read older than", + comment: "Mark articles as read older than") let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") - let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings") let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in cancelCompletion?() } - let settingsAction = UIAlertAction(title: settingsTitle, style: .default) { _ in - coordinator.showSettings(scrollToArticlesSection: true) - } - let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: completion) + let oneDayAction = UIAlertAction(title: "1 Day", style: .default, handler: completion) + let twoDaysAction = UIAlertAction(title: "2 Days", style: .default, handler: completion) + let threeDaysAction = UIAlertAction(title: "3 Days", style: .default, handler: completion) + let oneWeekAction = UIAlertAction(title: "1 Week", style: .default, handler: completion) + let twoWeeksAction = UIAlertAction(title: "2 Weeks", style: .default, handler: completion) + let oneMonthAction = UIAlertAction(title: "1 Month", style: .default, handler: completion) + let oneYearAction = UIAlertAction(title: "1 Year", style: .default, handler: completion) - alertController.addAction(markAction) - alertController.addAction(settingsAction) + alertController.addAction(oneDayAction) + alertController.addAction(twoDaysAction) + alertController.addAction(threeDaysAction) + alertController.addAction(oneWeekAction) + alertController.addAction(twoWeeksAction) + alertController.addAction(oneMonthAction) + alertController.addAction(oneYearAction) alertController.addAction(cancelAction) if let barButtonItem = sourceType as? UIBarButtonItem { From 1d0dbfa980129f269203acaad89fa9704b23d4ff Mon Sep 17 00:00:00 2001 From: Bryan Culver Date: Mon, 26 Sep 2022 22:49:27 -0400 Subject: [PATCH 02/13] Submenu prototype --- iOS/MasterFeed/MasterFeedViewController.swift | 76 ++++++++++++++++--- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 833bf0345..b93969f29 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -909,15 +909,15 @@ private extension MasterFeedViewController { if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) { markActions.append(markAllAction) } - - if let catchUpAction = self.catchUpAction(indexPath: indexPath) { - markActions.append(catchUpAction) - } if !markActions.isEmpty { menuElements.append(UIMenu(title: "", options: .displayInline, children: markActions)) } + if let catchUpAction = self.catchUpAction(indexPath: indexPath) { + menuElements.append(catchUpAction) + } + if includeDeleteRename { menuElements.append(UIMenu(title: "", options: .displayInline, @@ -1148,26 +1148,84 @@ private extension MasterFeedViewController { return action } - func catchUpAction(indexPath: IndexPath) -> UIAction? { + func catchUpAction(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 } - let localizedMenuText = NSLocalizedString("Catch Up in “%@”", comment: "Command") + let localizedMenuText = NSLocalizedString("Mark Older Than as Read in “%@”", comment: "Command") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String - let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in + let oneDayAction = UIAction(title: "1 Day") { [weak self] action in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in if let articles = try? feed.fetchUnreadArticles() { self?.coordinator.markAllAsRead(Array(articles)) } } } + let twoDayAction = UIAction(title: "2 Days") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in + if let articles = try? feed.fetchUnreadArticles() { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + let threeDayAction = UIAction(title: "3 Days") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in + if let articles = try? feed.fetchUnreadArticles() { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + let oneWeekAction = UIAction(title: "1 Week") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in + if let articles = try? feed.fetchUnreadArticles() { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + let twoWeekAction = UIAction(title: "2 Weeks") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in + if let articles = try? feed.fetchUnreadArticles() { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + let oneMonthAction = UIAction(title: "1 Month") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in + if let articles = try? feed.fetchUnreadArticles() { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } + let oneYearAction = UIAction(title: "1 Year") { [weak self] action in + MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in + if let articles = try? feed.fetchUnreadArticles() { + 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 action + 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 From 405b40e346136304818fa4d559f27ae2a4bbb490 Mon Sep 17 00:00:00 2001 From: Bryan Culver Date: Sat, 1 Oct 2022 15:17:58 -0400 Subject: [PATCH 03/13] Updated messaging --- iOS/MasterFeed/MasterFeedViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index b93969f29..5a4991429 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -1135,7 +1135,7 @@ private extension MasterFeedViewController { return nil } - let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") + let localizedMenuText = NSLocalizedString("Mark All as Read", comment: "Command") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in @@ -1155,7 +1155,7 @@ private extension MasterFeedViewController { return nil } - let localizedMenuText = NSLocalizedString("Mark Older Than as Read in “%@”", comment: "Command") + let localizedMenuText = NSLocalizedString("Mark Older Than as Read...", comment: "Command") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String let oneDayAction = UIAction(title: "1 Day") { [weak self] action in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in From 7eacc878d2c62d4ca2a8be59dcfe577b357d5680 Mon Sep 17 00:00:00 2001 From: Bryan Culver Date: Mon, 21 Nov 2022 22:06:45 -0500 Subject: [PATCH 04/13] Stashing --- Account/Sources/Account/Account.swift | 17 +++++++ Account/Sources/Account/ArticleFetcher.swift | 9 ++++ .../Account/SingleArticleFetcher.swift | 4 ++ Articles/Sources/Articles/Article.swift | 13 +++++ .../ArticlesDatabase/ArticlesDatabase.swift | 4 ++ .../ArticlesDatabase/ArticlesTable.swift | 23 ++++++++- .../ArticlesDatabase/StatusesTable.swift | 35 +++++++++++++ Shared/SmartFeeds/SmartFeed.swift | 4 ++ Shared/SmartFeeds/SmartFeedDelegate.swift | 4 ++ Shared/SmartFeeds/UnreadFeed.swift | 6 ++- iOS/MasterFeed/MasterFeedViewController.swift | 50 ++++++++++--------- .../MarkAsReadAlertController.swift | 27 ++++------ 12 files changed, 153 insertions(+), 43 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index c54941a04..24f2a29f2 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -1043,6 +1043,10 @@ private extension Account { return try fetchUnreadArticles(forContainer: self, limit: limit) } + func fetchUnreadArticlesBetween(limit: Int?, before: Date?, after: Date?) throws -> Set
{ + return try fetchUnreadArticlesBetween(forContainer: self, limit: limit, before: before, after: after) + } + func fetchUnreadArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { fetchUnreadArticlesAsync(forContainer: self, limit: limit, completion) } @@ -1152,6 +1156,19 @@ 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) + + // We don't validate limit queries because they, by definition, won't correctly match the + // complete unread state for the given container. + if limit == nil { + validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) + } + + 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/ArticleFetcher.swift b/Account/Sources/Account/ArticleFetcher.swift index 308cb155e..aefcaaf43 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) } @@ -36,6 +37,10 @@ extension WebFeed: ArticleFetcher { public func fetchUnreadArticles() throws -> Set
{ return try fetchArticles().unreadArticles() } + + public func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + return try fetchArticles().unreadArticlesBetween(before: before, after: after) + } public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { guard let account = account else { @@ -81,6 +86,10 @@ extension Folder: ArticleFetcher { return try account.fetchArticles(.folder(self, true)) } + public func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + return try fetchArticles().unreadArticlesBetween(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..31104fd09 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.fetchArticles(.articleIDs(Set([articleID]))) + } public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { return account.fetchArticlesAsync(.articleIDs(Set([articleID])), completion) diff --git a/Articles/Sources/Articles/Article.swift b/Articles/Sources/Articles/Article.swift index a979c7229..19bf2936b 100644 --- a/Articles/Sources/Articles/Article.swift +++ b/Articles/Sources/Articles/Article.swift @@ -79,6 +79,19 @@ public extension Set where Element == Article { let articles = self.filter { !$0.status.read } return Set(articles) } + + func unreadArticlesBetween(before: Date? = nil, after: Date? = nil) -> Set
{ + var articles = self.filter { !$0.status.read } + if before != nil { + // TODO: Address datePublished nil + articles = articles.filter { $0.datePublished != nil && $0.datePublished! <= before! } + } + if after != nil { + // TODO: Address datePublished nil + articles = articles.filter { $0.datePublished != nil && $0.datePublished! >= after! } + } + return Set(articles) + } func contains(accountID: String, articleID: String) -> Bool { return contains(where: { $0.accountID == accountID && $0.articleID == articleID}) diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift index fae1df9b2..ff4a7148f 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift @@ -106,6 +106,10 @@ public final class ArticlesDatabase { return try articlesTable.fetchUnreadArticles(webFeedIDs, limit) } + 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 a64be83dc..e7768af8d 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift @@ -75,11 +75,15 @@ final class ArticlesTable: DatabaseTable { } // MARK: - Fetching Unread Articles - + func fetchUnreadArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ 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 +849,23 @@ 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
() + } + let 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 limit = limit { + whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)") + } + if let before = before { + whereClause.append(" and dateArrived <= \(before)") + } + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) + } + func fetchArticlesForFeedID(_ webFeedID: String, _ database: FMDatabase) -> Set
{ return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject]) } diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift index bb53a36a2..95d06d788 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift @@ -167,6 +167,41 @@ final class StatusesTable: DatabaseTable { } } + func fetchArticleIDsArrivedBetweenDates(beforeDate: Date?, afterDate: Date?, completion: @escaping ArticleIDsCompletionBlock) { + queue.runInDatabase { databaseResult in + + var error: DatabaseError? + var articleIDs = Set() + + var sql = "select artcileID from statuses s" + + func makeDatabaseCall(_ database: FMDatabase) { + let sql = "select articleID from statuses s where (starred=1 or dateArrived>?) and not exists (select 1 from articles a where a.articleID = s.articleID);" + if let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) { + articleIDs = resultSet.mapToSet(self.articleIDWithRow) + } + } + + switch databaseResult { + case .success(let database): + makeDatabaseCall(database) + case .failure(let databaseError): + error = databaseError + } + + if let error = error { + DispatchQueue.main.async { + completion(.failure(error)) + } + } + else { + DispatchQueue.main.async { + completion(.success(articleIDs)) + } + } + } + } + func fetchArticleIDs(_ sql: String) throws -> Set { var error: DatabaseError? var articleIDs = Set() diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index e8245a0b9..e77b218e4 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -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..dfd7b4822 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 fetchArticles().unreadArticlesBetween(before: before, after: after) + } + func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { fetchArticlesAsync{ articleSetResult in switch articleSetResult { diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index eb9f4fb9c..00e20c509 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -73,10 +73,14 @@ extension UnreadFeed: ArticleFetcher { func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { fetchUnreadArticlesAsync(completion) } - + func fetchUnreadArticles() throws -> Set
{ return try AccountManager.shared.fetchArticles(fetchType) } + + func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + return try AccountManager.shared.fetchArticles(fetchType) + } func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { AccountManager.shared.fetchArticlesAsync(fetchType, completion) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 5a4991429..8af457743 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -905,16 +905,12 @@ private extension MasterFeedViewController { menuElements.append(UIMenu(title: "", options: .displayInline, children: pageActions)) } - var markActions = [UIAction]() if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) { - markActions.append(markAllAction) - } - if !markActions.isEmpty { - menuElements.append(UIMenu(title: "", options: .displayInline, children: markActions)) - + menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction])) + } - if let catchUpAction = self.catchUpAction(indexPath: indexPath) { + if let catchUpAction = self.catchUpActionMenu(indexPath: indexPath) { menuElements.append(catchUpAction) } @@ -1148,60 +1144,66 @@ private extension MasterFeedViewController { return action } - func catchUpAction(indexPath: IndexPath) -> UIMenu? { + 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 } - let localizedMenuText = NSLocalizedString("Mark Older Than as Read...", comment: "Command") - let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String + let title = NSLocalizedString("Mark Older Than as Read...", comment: "Command") let oneDayAction = UIAction(title: "1 Day") { [weak self] action in - MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in - if let articles = try? feed.fetchUnreadArticles() { + 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: title, sourceType: contentView) { [weak self] in - if let articles = try? feed.fetchUnreadArticles() { + 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: title, sourceType: contentView) { [weak self] in - if let articles = try? feed.fetchUnreadArticles() { + 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: title, sourceType: contentView) { [weak self] in - if let articles = try? feed.fetchUnreadArticles() { + 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: title, sourceType: contentView) { [weak self] in - if let articles = try? feed.fetchUnreadArticles() { + 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: title, sourceType: contentView) { [weak self] in - if let articles = try? feed.fetchUnreadArticles() { + 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: title, sourceType: contentView) { [weak self] in - if let articles = try? feed.fetchUnreadArticles() { + 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)) } } diff --git a/iOS/MasterTimeline/MarkAsReadAlertController.swift b/iOS/MasterTimeline/MarkAsReadAlertController.swift index fe37f2df8..5bbce1406 100644 --- a/iOS/MasterTimeline/MarkAsReadAlertController.swift +++ b/iOS/MasterTimeline/MarkAsReadAlertController.swift @@ -46,30 +46,23 @@ struct MarkAsReadAlertController { completion: @escaping (UIAlertAction) -> Void) -> UIAlertController where T: MarkAsReadAlertControllerSourceType { - let title = NSLocalizedString("Mark as Read", comment: "Catch Up") - let message = NSLocalizedString("Mark articles as read older than", - comment: "Mark articles as read older than") + let title = NSLocalizedString("Mark As Read", comment: "Mark As Read") + let message = NSLocalizedString("You can turn this confirmation off in Settings.", + comment: "You can turn this confirmation off in Settings.") let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") + let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings") let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in cancelCompletion?() } - let oneDayAction = UIAlertAction(title: "1 Day", style: .default, handler: completion) - let twoDaysAction = UIAlertAction(title: "2 Days", style: .default, handler: completion) - let threeDaysAction = UIAlertAction(title: "3 Days", style: .default, handler: completion) - let oneWeekAction = UIAlertAction(title: "1 Week", style: .default, handler: completion) - let twoWeeksAction = UIAlertAction(title: "2 Weeks", style: .default, handler: completion) - let oneMonthAction = UIAlertAction(title: "1 Month", style: .default, handler: completion) - let oneYearAction = UIAlertAction(title: "1 Year", style: .default, handler: completion) + let settingsAction = UIAlertAction(title: settingsTitle, style: .default) { _ in + coordinator.showSettings(scrollToArticlesSection: true) + } + let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: completion) - alertController.addAction(oneDayAction) - alertController.addAction(twoDaysAction) - alertController.addAction(threeDaysAction) - alertController.addAction(oneWeekAction) - alertController.addAction(twoWeeksAction) - alertController.addAction(oneMonthAction) - alertController.addAction(oneYearAction) + alertController.addAction(markAction) + alertController.addAction(settingsAction) alertController.addAction(cancelAction) if let barButtonItem = sourceType as? UIBarButtonItem { From 35a6cf551b8d5b4e896305fc36bdb217cbbf56dd Mon Sep 17 00:00:00 2001 From: Bryan Culver Date: Wed, 7 Dec 2022 00:30:19 -0500 Subject: [PATCH 05/13] Working implementations --- Account/Sources/Account/Account.swift | 32 ++-- Account/Sources/Account/AccountManager.swift | 10 ++ Account/Sources/Account/ArticleFetcher.swift | 10 +- .../Account/SingleArticleFetcher.swift | 2 +- Articles/Sources/Articles/Article.swift | 13 -- .../ArticlesDatabase/ArticlesDatabase.swift | 4 + .../ArticlesDatabase/ArticlesTable.swift | 41 +++++- .../ArticlesDatabase/StatusesTable.swift | 35 ----- ...idebarViewController+ContextualMenus.swift | 113 ++++++++++++--- Shared/SmartFeeds/SearchFeedDelegate.swift | 5 + .../SearchTimelineFeedDelegate.swift | 5 + Shared/SmartFeeds/SmartFeed.swift | 2 +- Shared/SmartFeeds/SmartFeedDelegate.swift | 2 +- Shared/SmartFeeds/StarredFeedDelegate.swift | 5 + Shared/SmartFeeds/UnreadFeed.swift | 6 +- iOS/MasterFeed/MasterFeedViewController.swift | 137 ++++++++++++++++-- 16 files changed, 319 insertions(+), 103 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 2f56cac0c..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): @@ -1041,10 +1057,6 @@ private extension Account { return try fetchUnreadArticles(forContainer: self, limit: limit) } - func fetchUnreadArticlesBetween(limit: Int?, before: Date?, after: Date?) throws -> Set
{ - return try fetchUnreadArticlesBetween(forContainer: self, limit: limit, before: before, after: after) - } - func fetchUnreadArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { fetchUnreadArticlesAsync(forContainer: self, limit: limit, completion) } @@ -1157,13 +1169,11 @@ private extension Account { 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) - - // We don't validate limit queries because they, by definition, won't correctly match the - // complete unread state for the given container. - if limit == nil { - validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) - } - + 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 } diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index 836cf0603..dfe828805 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -396,6 +396,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 aefcaaf43..53b41d810 100644 --- a/Account/Sources/Account/ArticleFetcher.swift +++ b/Account/Sources/Account/ArticleFetcher.swift @@ -37,9 +37,9 @@ extension WebFeed: ArticleFetcher { public func fetchUnreadArticles() throws -> Set
{ return try fetchArticles().unreadArticles() } - + public func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ - return try fetchArticles().unreadArticlesBetween(before: before, after: after) + return try account?.fetchUnreadArticlesBetween(webFeeds: [self], limit: nil, before: before, after: after) ?? Set
() } public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { @@ -87,7 +87,11 @@ extension Folder: ArticleFetcher { } public func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ - return try fetchArticles().unreadArticlesBetween(before: before, after: after) + 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) { diff --git a/Account/Sources/Account/SingleArticleFetcher.swift b/Account/Sources/Account/SingleArticleFetcher.swift index 31104fd09..ab3e26e3b 100644 --- a/Account/Sources/Account/SingleArticleFetcher.swift +++ b/Account/Sources/Account/SingleArticleFetcher.swift @@ -33,7 +33,7 @@ public struct SingleArticleFetcher: ArticleFetcher { } public func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ - return try account.fetchArticles(.articleIDs(Set([articleID]))) + return try account.fetchArticlesBetween(articleIDs: Set([articleID]), before: before, after: after) } public func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { diff --git a/Articles/Sources/Articles/Article.swift b/Articles/Sources/Articles/Article.swift index 19bf2936b..a979c7229 100644 --- a/Articles/Sources/Articles/Article.swift +++ b/Articles/Sources/Articles/Article.swift @@ -79,19 +79,6 @@ public extension Set where Element == Article { let articles = self.filter { !$0.status.read } return Set(articles) } - - func unreadArticlesBetween(before: Date? = nil, after: Date? = nil) -> Set
{ - var articles = self.filter { !$0.status.read } - if before != nil { - // TODO: Address datePublished nil - articles = articles.filter { $0.datePublished != nil && $0.datePublished! <= before! } - } - if after != nil { - // TODO: Address datePublished nil - articles = articles.filter { $0.datePublished != nil && $0.datePublished! >= after! } - } - return Set(articles) - } func contains(accountID: String, articleID: String) -> Bool { return contains(where: { $0.accountID == accountID && $0.articleID == articleID}) diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift index 1eae01250..33084286d 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift @@ -106,6 +106,10 @@ 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) } diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift index e41a57a25..ac59619da 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift @@ -70,12 +70,16 @@ 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) } // MARK: - Fetching Unread Articles - + func fetchUnreadArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, limit, $0) } } @@ -854,15 +858,22 @@ private extension ArticlesTable { if webFeedIDs.isEmpty { return Set
() } - let parameters = webFeedIDs.map { $0 as AnyObject } + 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)") } - if let before = before { - whereClause.append(" and dateArrived <= \(before)") - } return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } @@ -880,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/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift index d170718e4..c09ae1ed4 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift @@ -166,41 +166,6 @@ final class StatusesTable: DatabaseTable { } } - func fetchArticleIDsArrivedBetweenDates(beforeDate: Date?, afterDate: Date?, completion: @escaping ArticleIDsCompletionBlock) { - queue.runInDatabase { databaseResult in - - var error: DatabaseError? - var articleIDs = Set() - - var sql = "select artcileID from statuses s" - - func makeDatabaseCall(_ database: FMDatabase) { - let sql = "select articleID from statuses s where (starred=1 or dateArrived>?) and not exists (select 1 from articles a where a.articleID = s.articleID);" - if let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) { - articleIDs = resultSet.mapToSet(self.articleIDWithRow) - } - } - - switch databaseResult { - case .success(let database): - makeDatabaseCall(database) - case .failure(let databaseError): - error = databaseError - } - - if let error = error { - DispatchQueue.main.async { - completion(.failure(error)) - } - } - else { - DispatchQueue.main.async { - completion(.success(articleIDs)) - } - } - } - } - func fetchArticleIDs(_ sql: String) throws -> Set { var error: DatabaseError? var articleIDs = Set() diff --git a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift index 9163e33c7..df4f947ad 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: .year, 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 @@ -219,13 +269,13 @@ private extension SidebarViewController { let menu = NSMenu(title: "") if webFeed.unreadCount > 0 { -// menu.addItem(markAllReadMenuItem([webFeed])) - let catchUpMenuItem = catchUpMenuItem([webFeed]) + menu.addItem(markAllReadMenuItem([webFeed])) + let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark Older Than as Read...", comment: "Command Submenu"), action: nil, keyEquivalent: "") let catchUpSubMenu = catchUpSubMenu([webFeed]) - + menu.addItem(catchUpMenuItem) menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem) - + menu.addItem(NSMenuItem.separator()) } @@ -282,6 +332,11 @@ private extension SidebarViewController { if folder.unreadCount > 0 { menu.addItem(markAllReadMenuItem([folder])) + let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark Older Than as Read...", comment: "Command Submenu"), action: nil, keyEquivalent: "") + let catchUpSubMenu = catchUpSubMenu([folder]) + + menu.addItem(catchUpMenuItem) + menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem) menu.addItem(NSMenuItem.separator()) } @@ -297,6 +352,11 @@ private extension SidebarViewController { if smartFeed.unreadCount > 0 { menu.addItem(markAllReadMenuItem([smartFeed])) + let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark Older Than as Read...", comment: "Command Submenu"), action: nil, keyEquivalent: "") + let catchUpSubMenu = catchUpSubMenu([smartFeed]) + + menu.addItem(catchUpMenuItem) + menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem) } return menu.numberOfItems > 0 ? menu : nil } @@ -307,6 +367,11 @@ private extension SidebarViewController { if anyObjectInArrayHasNonZeroUnreadCount(objects) { menu.addItem(markAllReadMenuItem(objects)) + let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark Older Than as Read...", comment: "Command Submenu"), action: nil, keyEquivalent: "") + let catchUpSubMenu = catchUpSubMenu(objects) + + menu.addItem(catchUpMenuItem) + menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem) } if allObjectsAreFeedsAndOrFolders(objects) { @@ -321,27 +386,20 @@ private extension SidebarViewController { return menuItem(NSLocalizedString("Mark All as Read", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects) } - + func catchUpSubMenu(_ objects: [Any]) -> NSMenu { - let menu = NSMenu(title: "Catch up to articles older than...") - - menu.addItem(menuItem(NSLocalizedString("All", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) - menu.addItem(menuItem(NSLocalizedString("Older than 1 day", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) - menu.addItem(menuItem(NSLocalizedString("Older than 2 days", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) - menu.addItem(menuItem(NSLocalizedString("Older than 3 days", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) - menu.addItem(menuItem(NSLocalizedString("Older than 1 week", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) - menu.addItem(menuItem(NSLocalizedString("Older than 2 weeks", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) - menu.addItem(menuItem(NSLocalizedString("Older than 1 month", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) - menu.addItem(menuItem(NSLocalizedString("Older than 1 year", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)) - + + 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 catchUpMenuItem(_ objects: [Any]) -> NSMenuItem { - - return menuItem(NSLocalizedString("Mark as Read...", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects) - } func deleteMenuItem(_ objects: [Any]) -> NSMenuItem { @@ -400,5 +458,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 246ccca75..67fd628c9 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
{ + // TODO FILTER BY SEARCH + return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after) + } } diff --git a/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift b/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift index c186fa838..9e03ffdfb 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
{ + // TODO FILTER BY SEARCH + return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after) + } } diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index e77b218e4..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) { diff --git a/Shared/SmartFeeds/SmartFeedDelegate.swift b/Shared/SmartFeeds/SmartFeedDelegate.swift index dfd7b4822..1f9f9b1f0 100644 --- a/Shared/SmartFeeds/SmartFeedDelegate.swift +++ b/Shared/SmartFeeds/SmartFeedDelegate.swift @@ -32,7 +32,7 @@ extension SmartFeedDelegate { } func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ - return try fetchArticles().unreadArticlesBetween(before: before, after: after) + return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after) } func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { diff --git a/Shared/SmartFeeds/StarredFeedDelegate.swift b/Shared/SmartFeeds/StarredFeedDelegate.swift index 55770fe36..a36730aa3 100644 --- a/Shared/SmartFeeds/StarredFeedDelegate.swift +++ b/Shared/SmartFeeds/StarredFeedDelegate.swift @@ -29,4 +29,9 @@ struct StarredFeedDelegate: SmartFeedDelegate { func fetchUnreadCount(for account: Account, completion: @escaping SingleUnreadCountCompletionBlock) { account.fetchUnreadCountForStarredArticles(completion) } + + func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ + // TODO FILTER BY STARRED + return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after) + } } diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index 00e20c509..418671fc7 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -73,13 +73,13 @@ extension UnreadFeed: ArticleFetcher { func fetchArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { fetchUnreadArticlesAsync(completion) } - + func fetchUnreadArticles() throws -> Set
{ return try AccountManager.shared.fetchArticles(fetchType) } - + func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ - return try AccountManager.shared.fetchArticles(fetchType) + return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after) } func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index f41ade15e..4acd19458 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) @@ -923,7 +926,7 @@ private extension MasterFeedViewController { menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction])) } - + if let catchUpAction = self.catchUpActionMenu(indexPath: indexPath) { menuElements.append(catchUpAction) } @@ -953,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, @@ -966,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) }) } @@ -1162,7 +1178,14 @@ private extension MasterFeedViewController { 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 Older Than as Read...", 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 @@ -1232,7 +1255,7 @@ private extension MasterFeedViewController { return majorMenu } - + func getMarkOlderImageDirection() -> UIImage { if AppDefaults.shared.timelineSortDirection == .orderedDescending { return AppAssets.markBelowAsReadImage @@ -1260,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 Older Than as Read...", 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 } From e5d0efdcfde5114a88e9072c796e25b2a86392da Mon Sep 17 00:00:00 2001 From: Bryan Culver Date: Wed, 7 Dec 2022 00:47:46 -0500 Subject: [PATCH 06/13] Skip on today --- .../Sidebar/SidebarViewController+ContextualMenus.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift index df4f947ad..eed419875 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift @@ -352,9 +352,16 @@ 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 Older Than as Read...", comment: "Command Submenu"), action: nil, keyEquivalent: "") let catchUpSubMenu = catchUpSubMenu([smartFeed]) - menu.addItem(catchUpMenuItem) menu.setSubmenu(catchUpSubMenu, for: catchUpMenuItem) } From ae21bbd546d90487d99ed2eb4617cb6eb0f25327 Mon Sep 17 00:00:00 2001 From: Bryan Culver Date: Fri, 9 Dec 2022 20:56:12 -0500 Subject: [PATCH 07/13] Review updates --- .../Sidebar/SidebarViewController+ContextualMenus.swift | 2 +- Shared/SmartFeeds/StarredFeedDelegate.swift | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift index eed419875..c6280d660 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift @@ -94,7 +94,7 @@ extension SidebarViewController { } @objc func markObjectsReadOlderThanThreeDaysFromContextualMenu(_ sender: Any?) { - return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .year, value: -3, to: Date()), after: nil, sender: sender) + return markObjectsReadBetweenDatesFromContextualMenu(before: Calendar.current.date(byAdding: .day, value: -3, to: Date()), after: nil, sender: sender) } @objc func markObjectsReadOlderThanOneWeekFromContextualMenu(_ sender: Any?) { diff --git a/Shared/SmartFeeds/StarredFeedDelegate.swift b/Shared/SmartFeeds/StarredFeedDelegate.swift index a36730aa3..bb6b9a59f 100644 --- a/Shared/SmartFeeds/StarredFeedDelegate.swift +++ b/Shared/SmartFeeds/StarredFeedDelegate.swift @@ -31,7 +31,6 @@ struct StarredFeedDelegate: SmartFeedDelegate { } func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ - // TODO FILTER BY STARRED - return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after) + return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after).filter({ $0.status.starred }) } } From 40efe2203e29d631c6cade4f7e0265b2f6f61290 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 12 Dec 2022 16:23:16 -0700 Subject: [PATCH 08/13] Add fatalError to functions that should never be called because they are Searches. --- Shared/SmartFeeds/SearchFeedDelegate.swift | 4 ++-- Shared/SmartFeeds/SearchTimelineFeedDelegate.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Shared/SmartFeeds/SearchFeedDelegate.swift b/Shared/SmartFeeds/SearchFeedDelegate.swift index 67fd628c9..2f026d9f8 100644 --- a/Shared/SmartFeeds/SearchFeedDelegate.swift +++ b/Shared/SmartFeeds/SearchFeedDelegate.swift @@ -37,8 +37,8 @@ struct SearchFeedDelegate: SmartFeedDelegate { } func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ - // TODO FILTER BY SEARCH - return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after) + fatalError("Function not implemented.") } + } diff --git a/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift b/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift index 9e03ffdfb..5ac8e03bc 100644 --- a/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift +++ b/Shared/SmartFeeds/SearchTimelineFeedDelegate.swift @@ -37,7 +37,7 @@ struct SearchTimelineFeedDelegate: SmartFeedDelegate { } func fetchUnreadArticlesBetween(before: Date? = nil, after: Date? = nil) throws -> Set
{ - // TODO FILTER BY SEARCH - return try AccountManager.shared.fetchUnreadArticlesBetween(limit: nil, before: before, after: after) + fatalError("Function not implemented.") } + } From 339fd937a763f069bc926500d2cb73b44f03bd8f Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 12 Dec 2022 16:35:32 -0700 Subject: [PATCH 09/13] Change the Catch Up command menu option to something that flows a little better. --- .../Sidebar/SidebarViewController+ContextualMenus.swift | 8 ++++---- iOS/MasterFeed/MasterFeedViewController.swift | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift index c6280d660..67e6dc90e 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift @@ -270,7 +270,7 @@ private extension SidebarViewController { if webFeed.unreadCount > 0 { menu.addItem(markAllReadMenuItem([webFeed])) - let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark Older Than as Read...", comment: "Command Submenu"), action: nil, keyEquivalent: "") + let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark as Read Older Than", comment: "Command Submenu"), action: nil, keyEquivalent: "") let catchUpSubMenu = catchUpSubMenu([webFeed]) menu.addItem(catchUpMenuItem) @@ -332,7 +332,7 @@ private extension SidebarViewController { if folder.unreadCount > 0 { menu.addItem(markAllReadMenuItem([folder])) - let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark Older Than as Read...", comment: "Command Submenu"), action: nil, keyEquivalent: "") + let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark as Read Older Than", comment: "Command Submenu"), action: nil, keyEquivalent: "") let catchUpSubMenu = catchUpSubMenu([folder]) menu.addItem(catchUpMenuItem) @@ -360,7 +360,7 @@ private extension SidebarViewController { } } - let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark Older Than as Read...", comment: "Command Submenu"), action: nil, keyEquivalent: "") + 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) @@ -374,7 +374,7 @@ private extension SidebarViewController { if anyObjectInArrayHasNonZeroUnreadCount(objects) { menu.addItem(markAllReadMenuItem(objects)) - let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark Older Than as Read...", comment: "Command Submenu"), action: nil, keyEquivalent: "") + let catchUpMenuItem = NSMenuItem(title: NSLocalizedString("Mark as Read Older Than", comment: "Command Submenu"), action: nil, keyEquivalent: "") let catchUpSubMenu = catchUpSubMenu(objects) menu.addItem(catchUpMenuItem) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 4acd19458..b9f469fca 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -1186,7 +1186,7 @@ private extension MasterFeedViewController { } } - let title = NSLocalizedString("Mark Older Than as Read...", comment: "Command") + 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()) @@ -1288,7 +1288,7 @@ private extension MasterFeedViewController { return nil } - let title = NSLocalizedString("Mark Older Than as Read...", comment: "Command") + 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 From 51de7de1a1f2c80edb636f498359bf485b32a7a2 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 12 Dec 2022 16:35:52 -0700 Subject: [PATCH 10/13] Fix error handling for timed refreshes --- Shared/Timer/AccountRefreshTimer.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Shared/Timer/AccountRefreshTimer.swift b/Shared/Timer/AccountRefreshTimer.swift index 04c944a5a..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) - AccountManager.shared.refreshAll(completion: nil) + AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) } } From 90101274b4d88f2a2abb7c6422733ea0c575db81 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 12 Dec 2022 17:02:51 -0700 Subject: [PATCH 11/13] Add the title parameter to shortcut. Fixes #3697 --- iOS/Intents/AddWebFeedIntentHandler.swift | 31 +++++- .../Base.lproj/Intents.intentdefinition | 104 ++++++++++++------ iOS/IntentsExtension/Info.plist | 4 +- iOS/Resources/Info.plist | 50 ++++----- 4 files changed, 123 insertions(+), 66 deletions(-) diff --git a/iOS/Intents/AddWebFeedIntentHandler.swift b/iOS/Intents/AddWebFeedIntentHandler.swift index fb0711fdb..94a61bc8f 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/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/) From a614f0306de439fe46775e2c9fbb19f339f8b5a0 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 13 Dec 2022 14:12:38 -0700 Subject: [PATCH 12/13] Save sidebar state. Fixes #3740 --- iOS/SceneCoordinator.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 54c67f8f3..5a13b4d80 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -351,6 +351,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) @@ -2132,7 +2138,8 @@ private extension SceneCoordinator { return [ UserInfoKey.readFeedsFilterState: isReadFeedsFiltered, UserInfoKey.containerExpandedWindowState: containerExpandedWindowState, - UserInfoKey.readArticlesFilterState: readArticlesFilterState + UserInfoKey.readArticlesFilterState: readArticlesFilterState, + UserInfoKey.isSidebarHidden: rootSplitViewController.displayMode == .secondaryOnly ] } From b1a86031fee3a27ab4e57d446e7ae5d12e7d9be9 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 13 Dec 2022 14:27:54 -0700 Subject: [PATCH 13/13] Simplify the help menu and make it match the new iOS settings help section. --- Mac/AppDelegate.swift | 29 -------------------------- Mac/Base.lproj/Main.storyboard | 38 +--------------------------------- 2 files changed, 1 insertion(+), 66 deletions(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index d155b34a4..7cf49670e 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 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -