diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index b4929c8c7..5d46feaf2 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -103,6 +103,7 @@ 84A1500520048DDF0046AD9A /* SendToMarsEditCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */; }; 84A37CB5201ECD610087C5AF /* RenameWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A37CB4201ECD610087C5AF /* RenameWindowController.swift */; }; 84A37CBB201ECE590087C5AF /* RenameSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84A37CB9201ECE590087C5AF /* RenameSheet.xib */; }; + 84AAF2BF202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */; }; 84B06FAE1ED37DBD00F0B54B /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FA91ED37DAD00F0B54B /* RSCore.framework */; }; 84B06FAF1ED37DBD00F0B54B /* RSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FA91ED37DAD00F0B54B /* RSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 84B06FB21ED37DBD00F0B54B /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06F9D1ED37DA000F0B54B /* RSDatabase.framework */; }; @@ -135,6 +136,8 @@ 84DAEE321F870B390058304B /* DockBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAEE311F870B390058304B /* DockBadge.swift */; }; 84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */; }; 84E850861FCB60CE0072EA88 /* AuthorAvatarDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */; }; + 84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */; }; + 84E8E0EB202F693600562D8F /* DetailWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0EA202F693600562D8F /* DetailWebView.swift */; }; 84E95CF71FABB3C800552D99 /* FeedList.plist in Resources */ = {isa = PBXBuildFile; fileRef = 84E95CF61FABB3C800552D99 /* FeedList.plist */; }; 84E95D241FB1087500552D99 /* ArticlePasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */; }; 84EB381F1FBA8B9F000D2111 /* KeyboardShortcuts.html in Resources */ = {isa = PBXBuildFile; fileRef = 84EB38101FBA8B9F000D2111 /* KeyboardShortcuts.html */; }; @@ -619,6 +622,7 @@ 84A37CBA201ECE590087C5AF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Evergreen/Base.lproj/RenameSheet.xib; sourceTree = SOURCE_ROOT; }; 84A6B6931FB8D43C006754AC /* DinosaursWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DinosaursWindow.xib; sourceTree = ""; }; 84A6B6951FB8DBD2006754AC /* DinosaursWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DinosaursWindowController.swift; sourceTree = ""; }; + 84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContextualMenuDelegate.swift; sourceTree = ""; }; 84B06F961ED37DA000F0B54B /* RSDatabase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSDatabase.xcodeproj; path = Frameworks/RSDatabase/RSDatabase.xcodeproj; sourceTree = ""; }; 84B06FA21ED37DAC00F0B54B /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = Frameworks/RSCore/RSCore.xcodeproj; sourceTree = ""; }; 84B06FB61ED37E8B00F0B54B /* RSWeb.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSWeb.xcodeproj; path = Frameworks/RSWeb/RSWeb.xcodeproj; sourceTree = ""; }; @@ -644,6 +648,8 @@ 84DAEE311F870B390058304B /* DockBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DockBadge.swift; path = Evergreen/DockBadge.swift; sourceTree = ""; }; 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDefaults.swift; path = Evergreen/AppDefaults.swift; sourceTree = ""; }; 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorAvatarDownloader.swift; sourceTree = ""; }; + 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineViewController+ContextualMenus.swift"; sourceTree = ""; }; + 84E8E0EA202F693600562D8F /* DetailWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailWebView.swift; sourceTree = ""; }; 84E95CF61FABB3C800552D99 /* FeedList.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = FeedList.plist; sourceTree = ""; }; 84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlePasteboardWriter.swift; sourceTree = ""; }; 84EB38101FBA8B9F000D2111 /* KeyboardShortcuts.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = KeyboardShortcuts.html; sourceTree = ""; }; @@ -962,12 +968,14 @@ isa = PBXGroup; children = ( 849A976B1ED9EBC8007D329B /* TimelineViewController.swift */, + 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */, 84F204DF1FAACBB30076E152 /* ArticleArray.swift */, 849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */, 849A976A1ED9EBC8007D329B /* TimelineTableView.swift */, 844B5B6C1FEA282400C7C76A /* Keyboard */, 84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */, 8414AD241FCF5A1E00955102 /* TimelineHeaderView.swift */, + 84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */, 849A976F1ED9EC04007D329B /* Cell */, ); path = Timeline; @@ -990,6 +998,7 @@ isa = PBXGroup; children = ( 849A977E1ED9EC42007D329B /* DetailViewController.swift */, + 84E8E0EA202F693600562D8F /* DetailWebView.swift */, 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */, 84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */, 849A979A1ED9EFEB007D329B /* styleSheet.css */, @@ -1906,6 +1915,7 @@ 84A1500520048DDF0046AD9A /* SendToMarsEditCommand.swift in Sources */, D5907DB22004BB37005947E5 /* ScriptingObjectContainer.swift in Sources */, 849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */, + 84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */, 849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */, 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */, 84F204CE1FAACB660076E152 /* FeedListViewController.swift in Sources */, @@ -1914,6 +1924,7 @@ 848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */, 849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */, 849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */, + 84AAF2BF202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift in Sources */, 849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */, 84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */, 841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */, @@ -1933,6 +1944,7 @@ D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */, 842611A01FCB72600086A189 /* FeaturedImageDownloader.swift in Sources */, 849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */, + 84E8E0EB202F693600562D8F /* DetailWebView.swift in Sources */, 84CC08061FF5D2E000C0C0ED /* FeedListSplitViewController.swift in Sources */, 849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */, 849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */, diff --git a/Evergreen/Base.lproj/MainWindow.storyboard b/Evergreen/Base.lproj/MainWindow.storyboard index bb942f01b..aa07aa7ad 100644 --- a/Evergreen/Base.lproj/MainWindow.storyboard +++ b/Evergreen/Base.lproj/MainWindow.storyboard @@ -1,7 +1,7 @@ - + - + @@ -21,9 +21,9 @@ - + - + @@ -167,25 +167,6 @@ - - - - - - - - - - - - - - - - @@ -193,13 +174,13 @@ - + + + - - @@ -592,6 +573,7 @@ + @@ -623,15 +605,37 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -705,12 +709,12 @@ - - - - - - - + + + + + + + diff --git a/Evergreen/MainWindow/Detail/DetailViewController.swift b/Evergreen/MainWindow/Detail/DetailViewController.swift index 6bcfe7645..64a6c82b9 100644 --- a/Evergreen/MainWindow/Detail/DetailViewController.swift +++ b/Evergreen/MainWindow/Detail/DetailViewController.swift @@ -16,7 +16,7 @@ final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDe @IBOutlet var containerView: DetailContainerView! - var webview: WKWebView! + var webview: DetailWebView! var noSelectionView: NoSelectionView! var article: Article? { @@ -54,7 +54,7 @@ final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDe userContentController.add(self, name: MessageName.mouseDidExit) configuration.userContentController = userContentController - webview = WKWebView(frame: self.view.bounds, configuration: configuration) + webview = DetailWebView(frame: self.view.bounds, configuration: configuration) webview.uiDelegate = self webview.navigationDelegate = self webview.translatesAutoresizingMaskIntoConstraints = false diff --git a/Evergreen/MainWindow/Detail/DetailWebView.swift b/Evergreen/MainWindow/Detail/DetailWebView.swift new file mode 100644 index 000000000..2492622d8 --- /dev/null +++ b/Evergreen/MainWindow/Detail/DetailWebView.swift @@ -0,0 +1,66 @@ +// +// DetailWebView.swift +// Evergreen +// +// Created by Brent Simmons on 2/10/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import AppKit +import WebKit + +// There’s no API for affecting a WKWebView’s contextual menu. +// (WebView had API for this.) +// +// This a minor hack. It hides unwanted menu items. +// The menu item identifiers are not documented anywhere; +// they could change, and this code would need updating. + +final class DetailWebView: WKWebView { + + // MARK: NSView + + override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { + + for menuItem in menu.items { + if shouldHideMenuItem(menuItem) { + menuItem.isHidden = true + } + } + + super.willOpenMenu(menu, with: event) + } +} + +private extension NSUserInterfaceItemIdentifier { + + static let DetailMenuItemIdentifierReload = NSUserInterfaceItemIdentifier(rawValue: "WKMenuItemIdentifierReload") + static let DetailMenuItemIdentifierOpenLink = NSUserInterfaceItemIdentifier(rawValue: "WKMenuItemIdentifierOpenLink") +} + +private extension DetailWebView { + + static let menuItemIdentifiersToHide: [NSUserInterfaceItemIdentifier] = [.DetailMenuItemIdentifierReload, .DetailMenuItemIdentifierOpenLink] + static let menuItemIdentifierMatchStrings = ["newwindow", "download"] + + func shouldHideMenuItem(_ menuItem: NSMenuItem) -> Bool { + + guard let identifier = menuItem.identifier else { + return false + } + + if DetailWebView.menuItemIdentifiersToHide.contains(identifier) { + return true + } + + let lowerIdentifier = identifier.rawValue.lowercased() + for matchString in DetailWebView.menuItemIdentifierMatchStrings { + if lowerIdentifier.contains(matchString) { + return true + } + } + + return false + } +} + diff --git a/Evergreen/MainWindow/Sidebar/SidebarContextualMenuDelegate.swift b/Evergreen/MainWindow/Sidebar/SidebarContextualMenuDelegate.swift index 1c2944646..de1dd5e8f 100644 --- a/Evergreen/MainWindow/Sidebar/SidebarContextualMenuDelegate.swift +++ b/Evergreen/MainWindow/Sidebar/SidebarContextualMenuDelegate.swift @@ -7,6 +7,7 @@ // import AppKit +import RSCore @objc final class SidebarContextualMenuDelegate: NSObject, NSMenuDelegate { @@ -24,11 +25,7 @@ import AppKit return } - let items = contextualMenu.items - contextualMenu.removeAllItems() - for menuItem in items { - menu.addItem(menuItem) - } + menu.takeItems(from: contextualMenu) } } diff --git a/Evergreen/MainWindow/Timeline/ArticleArray.swift b/Evergreen/MainWindow/Timeline/ArticleArray.swift index 2c30fa99d..b1a8d7512 100644 --- a/Evergreen/MainWindow/Timeline/ArticleArray.swift +++ b/Evergreen/MainWindow/Timeline/ArticleArray.swift @@ -78,13 +78,38 @@ extension Array where Element == Article { func canMarkAllAsRead() -> Bool { + return anyArticleIsUnread() + } + + func anyArticlePassesTest(_ test: ((Article) -> Bool)) -> Bool { + for article in self { - if !article.status.read { + if test(article) { return true } } return false } + + func anyArticleIsRead() -> Bool { + + return anyArticlePassesTest { $0.status.read } + } + + func anyArticleIsUnread() -> Bool { + + return anyArticlePassesTest { !$0.status.read } + } + + func anyArticleIsStarred() -> Bool { + + return anyArticlePassesTest { $0.status.starred } + } + + func anyArticleIsUnstarred() -> Bool { + + return anyArticlePassesTest { !$0.status.starred } + } } private extension Array where Element == Article { diff --git a/Evergreen/MainWindow/Timeline/TimelineContextualMenuDelegate.swift b/Evergreen/MainWindow/Timeline/TimelineContextualMenuDelegate.swift new file mode 100644 index 000000000..2510f5d77 --- /dev/null +++ b/Evergreen/MainWindow/Timeline/TimelineContextualMenuDelegate.swift @@ -0,0 +1,32 @@ +// +// TimelineContextualMenuDelegate.swift +// Evergreen +// +// Created by Brent Simmons on 2/8/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import AppKit +import RSCore + +@objc final class TimelineContextualMenuDelegate: NSObject, NSMenuDelegate { + + @IBOutlet weak var timelineViewController: TimelineViewController? + + public func menuNeedsUpdate(_ menu: NSMenu) { + + guard let timelineViewController = timelineViewController else { + return + } + + menu.removeAllItems() + + guard let contextualMenu = timelineViewController.contextualMenuForClickedRows() else { + return + } + + menu.takeItems(from: contextualMenu) + } +} + + diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift new file mode 100644 index 000000000..3a0df2348 --- /dev/null +++ b/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift @@ -0,0 +1,162 @@ +// +// TimelineViewController+ContextualMenus.swift +// Evergreen +// +// Created by Brent Simmons on 2/9/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import AppKit +import Data +import Account + +extension TimelineViewController { + + func contextualMenuForClickedRows() -> NSMenu? { + + let row = tableView.clickedRow + guard row != -1, let article = articles.articleAtRow(row) else { + return nil + } + + if selectedArticles.contains(article) { + // If the clickedRow is part of the selected rows, then do a contextual menu for all the selected rows. + return menu(for: selectedArticles) + } + return menu(for: [article]) + } +} + +// MARK: Contextual Menu Actions + +extension TimelineViewController { + + @objc func markArticlesReadFromContextualMenu(_ sender: Any?) { + + guard let articles = articles(from: sender) else { + return + } + markArticles(articles, read: true) + } + + @objc func markArticlesUnreadFromContextualMenu(_ sender: Any?) { + + guard let articles = articles(from: sender) else { + return + } + markArticles(articles, read: false) + } + + @objc func markArticlesStarredFromContextualMenu(_ sender: Any?) { + + } + + @objc func markArticlesUnstarredFromContextualMenu(_ sender: Any?) { + + } + + @objc func openInBrowserFromContextualMenu(_ sender: Any?) { + + guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else { + return + } + Browser.open(urlString, inBackground: false) + } +} + + +private extension TimelineViewController { + + func markArticles(_ articles: [Article], read: Bool) { + + guard let articlesToMark = read ? unreadArticles(from: articles) : readArticles(from: articles) else { + return + } + guard let undoManager = undoManager, let markReadCommand = MarkReadOrUnreadCommand(initialArticles: Array(articlesToMark), markingRead: read, undoManager: undoManager) else { + return + } + + runCommand(markReadCommand) + } + + func unreadArticles(from articles: [Article]) -> [Article]? { + + let filteredArticles = articles.filter { !$0.status.read } + return filteredArticles.isEmpty ? nil : filteredArticles + } + + func readArticles(from articles: [Article]) -> [Article]? { + + let filteredArticles = articles.filter { $0.status.read } + return filteredArticles.isEmpty ? nil : filteredArticles + } + + func articles(from sender: Any?) -> [Article]? { + + return (sender as? NSMenuItem)?.representedObject as? [Article] + } + + func menu(for articles: [Article]) -> NSMenu? { + + let menu = NSMenu(title: "") + + if articles.anyArticleIsUnread() { + menu.addItem(markReadMenuItem(articles)) + } + if articles.anyArticleIsRead() { + menu.addItem(markUnreadMenuItem(articles)) + } + if menu.items.count > 0 { + menu.addItem(NSMenuItem.separator()) + } + +// if articles.anyArticleIsUnstarred() { +// menu.addItem(markStarredMenuItem(articles)) +// } +// if articles.anyArticleIsStarred() { +// menu.addItem(markUnstarredMenuItem(articles)) +// } + if menu.items.count > 0 && !menu.items.last!.isSeparatorItem { + menu.addItem(NSMenuItem.separator()) + } + + if articles.count == 1, let link = articles.first!.preferredLink { + menu.addItem(openInBrowserMenuItem(link)) + } + + return menu + } + + func markReadMenuItem(_ articles: [Article]) -> NSMenuItem { + + return menuItem(NSLocalizedString("Mark as Read", comment: "Command"), #selector(markArticlesReadFromContextualMenu(_:)), articles) + } + + func markUnreadMenuItem(_ articles: [Article]) -> NSMenuItem { + + return menuItem(NSLocalizedString("Mark as Unread", comment: "Command"), #selector(markArticlesUnreadFromContextualMenu(_:)), articles) + } + + func markStarredMenuItem(_ articles: [Article]) -> NSMenuItem { + + return menuItem(NSLocalizedString("Mark as Starred", comment: "Command"), #selector(markArticlesStarredFromContextualMenu(_:)), articles) + } + + func markUnstarredMenuItem(_ articles: [Article]) -> NSMenuItem { + + return menuItem(NSLocalizedString("Mark as Unstarred", comment: "Command"), #selector(markArticlesUnstarredFromContextualMenu(_:)), articles) + } + + func openInBrowserMenuItem(_ urlString: String) -> NSMenuItem { + + return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlString) + } + + func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem { + + let item = NSMenuItem(title: title, action: action, keyEquivalent: "") + item.representedObject = representedObject + item.target = self + return item + } +} diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index afcf64d16..993a8d8ba 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -15,7 +15,8 @@ import Account class TimelineViewController: NSViewController, UndoableCommandRunner { @IBOutlet var tableView: TimelineTableView! - + @IBOutlet var contextualMenuDelegate: TimelineContextualMenuDelegate? + var selectedArticles: [Article] { get { return Array(articles.articlesForIndexes(tableView.selectedRowIndexes)) @@ -28,6 +29,16 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { } } + var articles = ArticleArray() { + didSet { + if articles != oldValue { + clearUndoableCommands() + updateShowAvatars() + tableView.reloadData() + } + } + } + var undoableCommands = [UndoableCommand]() private var cellAppearance: TimelineCellAppearance! private var cellAppearanceWithAvatar: TimelineCellAppearance! @@ -60,16 +71,6 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { } } } - private var articles = ArticleArray() { - didSet { - if articles != oldValue { - clearUndoableCommands() - updateShowAvatars() - tableView.reloadData() - } - } - } - private var fontSize: FontSize = AppDefaults.shared.timelineFontSize { didSet { if fontSize != oldValue { @@ -678,11 +679,8 @@ private extension TimelineViewController { for object in representedObjects { - if let feed = object as? Feed { - fetchedArticles.formUnion(feed.fetchArticles()) - } - else if let folder = object as? Folder { - fetchedArticles.formUnion(folder.fetchArticles()) + if let articleFetcher = object as? ArticleFetcher { + fetchedArticles.formUnion(articleFetcher.fetchArticles()) } } diff --git a/Evergreen/SmartFeeds/SmartFeed.swift b/Evergreen/SmartFeeds/SmartFeed.swift index e623399b0..99de45209 100644 --- a/Evergreen/SmartFeeds/SmartFeed.swift +++ b/Evergreen/SmartFeeds/SmartFeed.swift @@ -11,7 +11,7 @@ import RSCore import Data import Account -protocol SmartFeedDelegate: DisplayNameProvider { +protocol SmartFeedDelegate: DisplayNameProvider, ArticleFetcher { func fetchUnreadCount(for: Account, callback: @escaping (Int) -> Void) } @@ -51,6 +51,19 @@ final class SmartFeed: PseudoFeed { } } +extension SmartFeed: ArticleFetcher { + + func fetchArticles() -> Set
{ + + return delegate.fetchArticles() + } + + func fetchUnreadArticles() -> Set
{ + + return delegate.fetchUnreadArticles() + } +} + private extension SmartFeed { // MARK: - Unread Counts diff --git a/Evergreen/SmartFeeds/StarredFeedDelegate.swift b/Evergreen/SmartFeeds/StarredFeedDelegate.swift index 424ad530a..e09ca4db9 100644 --- a/Evergreen/SmartFeeds/StarredFeedDelegate.swift +++ b/Evergreen/SmartFeeds/StarredFeedDelegate.swift @@ -19,4 +19,18 @@ struct StarredFeedDelegate: SmartFeedDelegate { account.fetchUnreadCountForStarredArticles(callback) } + + // MARK: ArticleFetcher + + func fetchArticles() -> Set
{ + + // TODO + return Set
() + } + + func fetchUnreadArticles() -> Set
{ + + return fetchArticles().unreadArticles() + } + } diff --git a/Evergreen/SmartFeeds/TodayFeedDelegate.swift b/Evergreen/SmartFeeds/TodayFeedDelegate.swift index 58ae61095..9e74bca7c 100644 --- a/Evergreen/SmartFeeds/TodayFeedDelegate.swift +++ b/Evergreen/SmartFeeds/TodayFeedDelegate.swift @@ -18,5 +18,21 @@ struct TodayFeedDelegate: SmartFeedDelegate { account.fetchUnreadCountForToday(callback) } + + // MARK: ArticleFetcher + + func fetchArticles() -> Set
{ + + var articles = Set
() + for account in AccountManager.shared.accounts { + articles.formUnion(account.fetchTodayArticles()) + } + return articles + } + + func fetchUnreadArticles() -> Set
{ + + return fetchArticles().unreadArticles() + } } diff --git a/Evergreen/SmartFeeds/UnreadFeed.swift b/Evergreen/SmartFeeds/UnreadFeed.swift index d80b0e07c..eae26a601 100644 --- a/Evergreen/SmartFeeds/UnreadFeed.swift +++ b/Evergreen/SmartFeeds/UnreadFeed.swift @@ -7,6 +7,8 @@ // import Foundation +import Account +import Data // This just shows the global unread count, which appDelegate already has. Easy. @@ -34,3 +36,20 @@ final class UnreadFeed: PseudoFeed { unreadCount = appDelegate.unreadCount } } + +extension UnreadFeed: ArticleFetcher { + + func fetchArticles() -> Set
{ + + return fetchUnreadArticles() + } + + func fetchUnreadArticles() -> Set
{ + + var articles = Set
() + for account in AccountManager.shared.accounts { + articles.formUnion(account.fetchUnreadArticles()) + } + return articles + } +} diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index b547ef459..44c334222 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -341,14 +341,29 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return articles } + public func fetchUnreadArticles() -> Set
{ + + return fetchUnreadArticles(forContainer: self) + } + public func fetchArticles(folder: Folder) -> Set
{ - let feeds = folder.flattenedFeeds() + return fetchUnreadArticles(forContainer: folder) + } + + public func fetchUnreadArticles(forContainer container: Container) -> Set
{ + + let feeds = container.flattenedFeeds() let articles = database.fetchUnreadArticles(for: feeds) feeds.forEach { validateUnreadCount($0, articles) } return articles } + public func fetchTodayArticles() -> Set
{ + + return database.fetchTodayArticles(for: flattenedFeeds()) + } + private func validateUnreadCount(_ feed: Feed, _ articles: Set
) { // articles must contain all the unread articles for the feed. diff --git a/Frameworks/Data/Article.swift b/Frameworks/Data/Article.swift index 9e476f0ea..bee38db21 100644 --- a/Frameworks/Data/Article.swift +++ b/Frameworks/Data/Article.swift @@ -75,6 +75,12 @@ public extension Set where Element == Article { return Set(map { $0.articleID }) } + + public func unreadArticles() -> Set
{ + + let articles = self.filter { !$0.status.read } + return Set(articles) + } } public extension Array where Element == Article { diff --git a/Frameworks/Database/ArticlesTable.swift b/Frameworks/Database/ArticlesTable.swift index 620978c24..8bc0aeccb 100644 --- a/Frameworks/Database/ArticlesTable.swift +++ b/Frameworks/Database/ArticlesTable.swift @@ -72,6 +72,11 @@ final class ArticlesTable: DatabaseTable { return fetchUnreadArticles(feeds.feedIDs()) } + public func fetchTodayArticles(for feeds: Set) -> Set
{ + + return fetchTodayArticles(feeds.feedIDs()) + } + // MARK: Updating func update(_ feed: Feed, _ parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) { @@ -328,8 +333,14 @@ private extension ArticlesTable { // * Must not be deleted. // * Must be either 1) starred or 2) dateArrived must be newer than cutoff date. - let sql = withLimits ? "select * from articles natural join statuses where \(whereClause) and userDeleted=0 and (starred=1 or dateArrived>?);" : "select * from articles natural join statuses where \(whereClause);" - return articlesWithSQL(sql, parameters + [articleCutoffDate as AnyObject], database) + if withLimits { + let sql = "select * from articles natural join statuses where \(whereClause) and userDeleted=0 and (starred=1 or dateArrived>?);" + return articlesWithSQL(sql, parameters + [articleCutoffDate as AnyObject], database) + } + else { + let sql = "select * from articles natural join statuses where \(whereClause);" + return articlesWithSQL(sql, parameters, database) + } } func fetchUnreadCount(_ feedID: String, _ database: FMDatabase) -> Int { @@ -369,6 +380,31 @@ private extension ArticlesTable { return articles } + func fetchTodayArticles(_ feedIDs: Set) -> Set
{ + + if feedIDs.isEmpty { + return Set
() + } + + var articles = Set
() + + queue.fetchSync { (database) in + + // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?) + // + // datePublished may be nil, so we fall back to dateArrived. + + let startOfToday = NSCalendar.startOfToday() + let parameters = feedIDs.map { $0 as AnyObject } + [startOfToday as AnyObject, startOfToday as AnyObject] + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))! + let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and userDeleted = 0" +// let whereClause = "feedID in \(placeholders) and datePublished > ? and userDeleted = 0" + articles = self.fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) + } + + return articles + } + func articlesWithSQL(_ sql: String, _ parameters: [AnyObject], _ database: FMDatabase) -> Set
{ guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else { diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index 42ea758de..9eb372c1c 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -57,6 +57,11 @@ public final class Database { return articlesTable.fetchUnreadArticles(for: feeds) } + public func fetchTodayArticles(for feeds: Set) -> Set
{ + + return articlesTable.fetchTodayArticles(for: feeds) + } + // MARK: - Unread Counts public func fetchUnreadCounts(for feeds: Set, _ completion: @escaping UnreadCountCompletionBlock) { diff --git a/Frameworks/Database/Extensions/Article+Database.swift b/Frameworks/Database/Extensions/Article+Database.swift index bd61718f1..b92f0f1b0 100644 --- a/Frameworks/Database/Extensions/Article+Database.swift +++ b/Frameworks/Database/Extensions/Article+Database.swift @@ -23,7 +23,7 @@ extension Article { let authors = Author.authorsWithParsedAuthors(parsedItem.authors) let attachments = Attachment.attachmentsWithParsedAttachments(parsedItem.attachments) - self.init(accountID: accountID, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished, dateModified: parsedItem.dateModified, authors: authors, attachments: attachments, status: status) + self.init(accountID: accountID, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished ?? parsedItem.dateModified, dateModified: parsedItem.dateModified, authors: authors, attachments: attachments, status: status) } private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath, _ otherArticle: Article, _ key: String, _ dictionary: NSMutableDictionary) { diff --git a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj index 4512910e0..d5c387606 100755 --- a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj +++ b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj @@ -158,6 +158,7 @@ 84CFF56E1AC3D20A00CEA6C8 /* NSImage+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84CFF56C1AC3D20A00CEA6C8 /* NSImage+RSCore.m */; }; 84D5BA1E201E87E2009092BD /* URLPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D5BA1D201E87E2009092BD /* URLPasteboardWriter.swift */; }; 84E34DA61F9FA1070077082F /* UndoableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E34DA51F9FA1070077082F /* UndoableCommand.swift */; }; + 84E8E0D9202EC39800562D8F /* NSMenu+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0D8202EC39800562D8F /* NSMenu+Extensions.swift */; }; 84F20F831F16BA6200D8E682 /* PropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F821F16BA6200D8E682 /* PropertyList.swift */; }; 84FE9FC31C00453900081CE9 /* NSStoryboard+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84FE9FC11C00453900081CE9 /* NSStoryboard+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84FE9FC41C00453900081CE9 /* NSStoryboard+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FE9FC21C00453900081CE9 /* NSStoryboard+RSCore.m */; }; @@ -276,6 +277,7 @@ 84CFF56C1AC3D20A00CEA6C8 /* NSImage+RSCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSImage+RSCore.m"; sourceTree = ""; }; 84D5BA1D201E87E2009092BD /* URLPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLPasteboardWriter.swift; path = AppKit/URLPasteboardWriter.swift; sourceTree = ""; }; 84E34DA51F9FA1070077082F /* UndoableCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UndoableCommand.swift; path = RSCore/UndoableCommand.swift; sourceTree = ""; }; + 84E8E0D8202EC39800562D8F /* NSMenu+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "NSMenu+Extensions.swift"; path = "AppKit/NSMenu+Extensions.swift"; sourceTree = ""; }; 84F20F821F16BA6200D8E682 /* PropertyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyList.swift; sourceTree = ""; }; 84FE9FC11C00453900081CE9 /* NSStoryboard+RSCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSStoryboard+RSCore.h"; sourceTree = ""; }; 84FE9FC21C00453900081CE9 /* NSStoryboard+RSCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSStoryboard+RSCore.m"; sourceTree = ""; }; @@ -454,6 +456,7 @@ 8415CB891BF84D24007B1E98 /* NSEvent+RSCore.m */, 84CFF56B1AC3D20A00CEA6C8 /* NSImage+RSCore.h */, 84CFF56C1AC3D20A00CEA6C8 /* NSImage+RSCore.m */, + 84E8E0D8202EC39800562D8F /* NSMenu+Extensions.swift */, 842635581D7FA24800196285 /* NSOutlineView+Extensions.swift */, 849B08951BF7BCE30090CEE4 /* NSPasteboard+RSCore.h */, 849B08961BF7BCE30090CEE4 /* NSPasteboard+RSCore.m */, @@ -661,6 +664,7 @@ }; 84CFF4F31AC3C69700CEA6C8 = { CreatedOnToolsVersion = 6.2; + DevelopmentTeam = 9C84TZ7Q6Z; LastSwiftMigration = 0800; }; 84CFF4FE1AC3C69700CEA6C8 = { @@ -784,6 +788,7 @@ 84C687321FBAA3DF00345C9E /* LogWindowController.swift in Sources */, 84C687381FBC028900345C9E /* LogItem.swift in Sources */, 8432B1861DACA0E90057D6DF /* NSResponder-Extensions.swift in Sources */, + 84E8E0D9202EC39800562D8F /* NSMenu+Extensions.swift in Sources */, 84D5BA1E201E87E2009092BD /* URLPasteboardWriter.swift in Sources */, 849B08981BF7BCE30090CEE4 /* NSPasteboard+RSCore.m in Sources */, 842635571D7FA1C800196285 /* NSTableView+Extensions.swift in Sources */, diff --git a/Frameworks/RSCore/RSCore/AppKit/NSMenu+Extensions.swift b/Frameworks/RSCore/RSCore/AppKit/NSMenu+Extensions.swift new file mode 100644 index 000000000..9263432f8 --- /dev/null +++ b/Frameworks/RSCore/RSCore/AppKit/NSMenu+Extensions.swift @@ -0,0 +1,23 @@ +// +// NSMenu+Extensions.swift +// RSCore +// +// Created by Brent Simmons on 2/9/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +import AppKit + +public extension NSMenu { + + public func takeItems(from menu: NSMenu) { + + // The passed-in menu gets all its items removed. + + let items = menu.items + menu.removeAllItems() + for menuItem in items { + addItem(menuItem) + } + } +} diff --git a/Frameworks/RSCore/RSCore/NSView+RSCore.h b/Frameworks/RSCore/RSCore/NSView+RSCore.h index 8545f561d..db3070f6f 100755 --- a/Frameworks/RSCore/RSCore/NSView+RSCore.h +++ b/Frameworks/RSCore/RSCore/NSView+RSCore.h @@ -27,5 +27,6 @@ - (NSRect)rs_rectCentered:(NSRect)originalRect; +- (NSTableView *)rs_enclosingTableView; @end diff --git a/Frameworks/RSCore/RSCore/NSView+RSCore.m b/Frameworks/RSCore/RSCore/NSView+RSCore.m index 255502909..29c2b3e26 100755 --- a/Frameworks/RSCore/RSCore/NSView+RSCore.m +++ b/Frameworks/RSCore/RSCore/NSView+RSCore.m @@ -65,4 +65,18 @@ } +- (NSTableView *)rs_enclosingTableView { + + NSView *nomad = self.superview; + + while (nomad != nil) { + if ([nomad isKindOfClass:[NSTableView class]]) { + return (NSTableView *)nomad; + } + nomad = nomad.superview; + } + + return nil; +} + @end diff --git a/Frameworks/RSTextDrawing/RSTextDrawing/RSMultiLineView.m b/Frameworks/RSTextDrawing/RSTextDrawing/RSMultiLineView.m index ea9ec342c..27107e720 100644 --- a/Frameworks/RSTextDrawing/RSTextDrawing/RSMultiLineView.m +++ b/Frameworks/RSTextDrawing/RSTextDrawing/RSMultiLineView.m @@ -6,6 +6,7 @@ // Copyright © 2016 Ranchero Software, LLC. All rights reserved. // +@import RSCore; #import "RSMultiLineView.h" #import "RSMultiLineRenderer.h" #import "RSMultiLineRendererMeasurements.h" @@ -137,6 +138,16 @@ static NSAttributedString *emptyAttributedString = nil; } +- (NSMenu *)menuForEvent:(NSEvent *)event { + + NSTableView *tableView = [self rs_enclosingTableView]; + if (tableView) { + return [tableView menuForEvent:event]; + } + return nil; +} + + - (void)drawRect:(NSRect)r { if (self.selected) { diff --git a/Frameworks/RSTextDrawing/RSTextDrawing/RSSingleLineView.m b/Frameworks/RSTextDrawing/RSTextDrawing/RSSingleLineView.m index af7d442ce..7830563fe 100644 --- a/Frameworks/RSTextDrawing/RSTextDrawing/RSSingleLineView.m +++ b/Frameworks/RSTextDrawing/RSTextDrawing/RSSingleLineView.m @@ -124,6 +124,14 @@ static NSAttributedString *emptyAttributedString = nil; return self.intrinsicSize; } +- (NSMenu *)menuForEvent:(NSEvent *)event { + + NSTableView *tableView = [self rs_enclosingTableView]; + if (tableView) { + return [tableView menuForEvent:event]; + } + return nil; +} - (void)drawRect:(NSRect)r {