From 35a6cf551b8d5b4e896305fc36bdb217cbbf56dd Mon Sep 17 00:00:00 2001 From: Bryan Culver Date: Wed, 7 Dec 2022 00:30:19 -0500 Subject: [PATCH] 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 }