Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ extension SidebarViewController {
runCommand(markReadCommand)
}

@objc func markObjectsReadExceptStarredFromContextualMenu(_ sender: Any?) {

guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [Any] else {
return
}

let articles = unreadUnstarredArticles(for: objects)
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, 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
Expand Down Expand Up @@ -212,6 +225,9 @@ private extension SidebarViewController {

if feed.unreadCount > 0 {
menu.addItem(markAllReadMenuItem([feed]))
if anyObjectHasUnreadUnstarredArticles([feed]) {
menu.addItem(markAllAsReadExceptStarredMenuItem([feed]))
}
menu.addItem(NSMenuItem.separator())
}

Expand Down Expand Up @@ -264,6 +280,9 @@ private extension SidebarViewController {

if folder.unreadCount > 0 {
menu.addItem(markAllReadMenuItem([folder]))
if anyObjectHasUnreadUnstarredArticles([folder]) {
menu.addItem(markAllAsReadExceptStarredMenuItem([folder]))
}
menu.addItem(NSMenuItem.separator())
}

Expand All @@ -279,6 +298,9 @@ private extension SidebarViewController {

if smartFeed.unreadCount > 0 {
menu.addItem(markAllReadMenuItem([smartFeed]))
if anyObjectHasUnreadUnstarredArticles([smartFeed]) {
menu.addItem(markAllAsReadExceptStarredMenuItem([smartFeed]))
}
}
return menu.numberOfItems > 0 ? menu : nil
}
Expand All @@ -289,6 +311,9 @@ private extension SidebarViewController {

if anyObjectInArrayHasNonZeroUnreadCount(objects) {
menu.addItem(markAllReadMenuItem(objects))
if anyObjectHasUnreadUnstarredArticles(objects) {
menu.addItem(markAllAsReadExceptStarredMenuItem(objects))
}
}

if allObjectsAreFeedsAndOrFolders(objects) {
Expand All @@ -304,6 +329,11 @@ private extension SidebarViewController {
return menuItem(NSLocalizedString("Mark All as Read", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects, image: Assets.Images.markAllAsReadMenu)
}

func markAllAsReadExceptStarredMenuItem(_ objects: [Any]) -> NSMenuItem {

return menuItem(NSLocalizedString("Mark Unstarred as Read", comment: "Command"), #selector(markObjectsReadExceptStarredFromContextualMenu(_:)), objects, image: Assets.Images.markAllAsReadMenu)
}

func deleteMenuItem(_ objects: [Any]) -> NSMenuItem {

return menuItem(NSLocalizedString("Delete", comment: "Command"), #selector(deleteFromContextualMenu(_:)), objects, image: Assets.Images.delete)
Expand Down Expand Up @@ -364,4 +394,32 @@ private extension SidebarViewController {
}
return articles
}

func unreadUnstarredArticles(for objects: [Any]) -> Set<Article> {

var articles = Set<Article>()
for object in objects {
if let articleFetcher = object as? ArticleFetcher {
if let unreadArticles = try? articleFetcher.fetchUnreadArticles() {
let unstarred = unreadArticles.filter { !$0.status.starred }
articles.formUnion(unstarred)
}
}
}
return articles
}

func anyObjectHasUnreadUnstarredArticles(_ objects: [Any]) -> Bool {

for object in objects {
if let articleFetcher = object as? ArticleFetcher {
if let unreadArticles = try? articleFetcher.fetchUnreadArticles() {
if unreadArticles.contains(where: { !$0.status.starred }) {
return true
}
}
}
}
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ extension TimelineViewController {
markBelowArticlesRead(articles)
}

@objc func markAllAsReadExceptStarredFromContextualMenu(_ sender: Any?) {
guard let unreadUnstarred = self.articles.unreadUnstarredArticles() else {
return
}
markArticles(unreadUnstarred, read: true)
}

@objc func markArticlesStarredFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else { return }
markArticles(articles, starred: true)
Expand Down Expand Up @@ -167,6 +174,9 @@ private extension TimelineViewController {
if let last = articles.last, self.articles.articlesBelow(article: last).canMarkAllAsRead() {
menu.addItem(markBelowReadMenuItem(articles))
}
if self.articles.canMarkAllAsReadExceptStarred() {
menu.addItem(markAllAsReadExceptStarredMenuItem(articles))
}

menu.addSeparatorIfNeeded()

Expand Down Expand Up @@ -252,6 +262,10 @@ private extension TimelineViewController {
return menuItem(NSLocalizedString("Mark Below as Read", comment: "Command"), #selector(markBelowArticlesReadFromContextualMenu(_:)), articles, image: Assets.Images.markBelowAsRead)
}

func markAllAsReadExceptStarredMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("Mark Unstarred as Read", comment: "Command"), #selector(markAllAsReadExceptStarredFromContextualMenu(_:)), articles, image: Assets.Images.markAllAsReadMenu)
}

func selectFeedInSidebarMenuItem(_ feed: Feed) -> NSMenuItem {
let localizedMenuText = NSLocalizedString("Select “%@” in Sidebar", comment: "Command")
let formattedMenuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay)
Expand Down
9 changes: 9 additions & 0 deletions Shared/Timeline/ArticleArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ typealias ArticleArray = [Article]
return articles.isEmpty ? nil : articles
}

func canMarkAllAsReadExceptStarred() -> Bool {
return anyArticlePassesTest { !$0.status.read && !$0.status.starred }
}

func unreadUnstarredArticles() -> [Article]? {
let articles = self.filter { !$0.status.read && !$0.status.starred }
return articles.isEmpty ? nil : articles
}

func representSameArticlesInSameOrder(as otherArticles: [Article]) -> Bool {
if self.count != otherArticles.count {
return false
Expand Down
99 changes: 97 additions & 2 deletions iOS/MainFeed/MainFeedCollectionViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ final class MainFeedCollectionViewController: UICollectionViewController, Undoab
alert.addAction(action)
}

if let action = self.markUnstarredAsReadAlertAction(indexPath: indexPath, completion: completion) {
alert.addAction(action)
}

let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel) { _ in
completion(true)
Expand Down Expand Up @@ -860,6 +864,10 @@ extension MainFeedCollectionViewController: UIContextMenuInteractionDelegate {
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction]))
}

if let markUnstarredAction = self.markUnstarredAsReadAction(account: account, contentView: interaction.view) {
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markUnstarredAction]))
}

menuElements.append(UIMenu(title: "", options: .displayInline, children: [self.deactivateAccountAction(account: account)]))

return UIMenu(title: "", children: menuElements)
Expand Down Expand Up @@ -910,6 +918,10 @@ extension MainFeedCollectionViewController {
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction]))
}

if let markUnstarredAction = self.markUnstarredAsReadAction(indexPath: indexPath) {
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markUnstarredAction]))
}

if includeDeleteRename {
menuElements.append(UIMenu(title: "",
options: .displayInline,
Expand All @@ -936,6 +948,10 @@ extension MainFeedCollectionViewController {
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markAllAction]))
}

if let markUnstarredAction = self.markUnstarredAsReadAction(indexPath: indexPath) {
menuElements.append(UIMenu(title: "", options: .displayInline, children: [markUnstarredAction]))
}

menuElements.append(UIMenu(title: "",
options: .displayInline,
children: [
Expand All @@ -949,12 +965,21 @@ extension MainFeedCollectionViewController {
}

func makePseudoFeedContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration? {
guard let markAllAction = self.markAllAsReadAction(indexPath: indexPath) else {
var actions = [UIAction]()

if let markAllAction = self.markAllAsReadAction(indexPath: indexPath) {
actions.append(markAllAction)
}
if let markUnstarredAction = self.markUnstarredAsReadAction(indexPath: indexPath) {
actions.append(markUnstarredAction)
}

guard !actions.isEmpty else {
return nil
}

return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { _ in
return UIMenu(title: "", children: [markAllAction])
return UIMenu(title: "", children: actions)
})
}

Expand Down Expand Up @@ -1164,6 +1189,76 @@ extension MainFeedCollectionViewController {
return action
}

func markUnstarredAsReadAction(indexPath: IndexPath) -> UIAction? {
guard let sidebarItem = dataSource.itemIdentifier(for: indexPath)?.node.representedObject as? SidebarItem,
let contentView = self.collectionView.cellForItem(at: indexPath)?.contentView,
sidebarItem.unreadCount > 0,
let articles = try? sidebarItem.fetchUnreadArticles(),
Array(articles).unreadUnstarredArticles() != nil else {
return nil
}

let title = NSLocalizedString("Mark Unstarred as Read", comment: "Command")
let action = UIAction(title: title, image: Assets.Images.markAllAsRead) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
if let articles = try? sidebarItem.fetchUnreadArticles(),
let unstarred = Array(articles).unreadUnstarredArticles() {
self?.coordinator.markAllAsRead(unstarred)
}
}
}
return action
}

func markUnstarredAsReadAction(account: Account, contentView: UIView?) -> UIAction? {
guard account.unreadCount > 0, let contentView else {
return nil
}
guard let articles = try? account.fetchArticles(.unread()),
Array(articles).unreadUnstarredArticles() != nil else {
return nil
}

let title = NSLocalizedString("Mark Unstarred as Read", comment: "Command")
let action = UIAction(title: title, image: Assets.Images.markAllAsRead) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if let articles = try? account.fetchArticles(.unread()),
let unstarred = Array(articles).unreadUnstarredArticles() {
self?.coordinator.markAllAsRead(unstarred)
}
}
}
}
return action
}

func markUnstarredAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let feed = dataSource.itemIdentifier(for: indexPath)?.node.representedObject as? Feed,
feed.unreadCount > 0,
let articles = try? feed.fetchArticles(),
Array(articles).unreadUnstarredArticles() != nil,
let contentView = self.collectionView.cellForItem(at: indexPath)?.contentView else {
return nil
}

let title = NSLocalizedString("Mark Unstarred as Read", comment: "Command")
let cancel = {
completion(true)
}

let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
if let articles = try? feed.fetchArticles(),
let unstarred = Array(articles).unreadUnstarredArticles() {
self?.coordinator.markAllAsRead(unstarred)
}
completion(true)
}
}
return action
}

func rename(indexPath: IndexPath) {
guard let sidebarItem = dataSource.itemIdentifier(for: indexPath)?.node.representedObject as? SidebarItem else {
return
Expand Down
51 changes: 51 additions & 0 deletions iOS/MainTimeline/MainTimelineModernViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,11 @@ final class MainTimelineModernViewController: UIViewController, UndoableCommandR
coordinator?.markAllAsReadInTimeline()
}

private func markAllAsReadExceptStarredInTimeline() {
assert(coordinator != nil)
coordinator?.markAllAsReadExceptStarredInTimeline()
}

@IBAction func markAllAsRead(_ sender: Any?) {
let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")

Expand Down Expand Up @@ -516,6 +521,9 @@ extension MainTimelineModernViewController: UICollectionViewDelegate {
if let action = self.markAllInFeedAsReadAction(article, indexPath: firstIndex) {
secondaryActions.append(action)
}
if let action = self.markAllAsReadExceptStarredAction(firstIndex) {
secondaryActions.append(action)
}
if !secondaryActions.isEmpty {
menuElements.append(UIMenu(title: "", options: .displayInline, children: secondaryActions))
}
Expand Down Expand Up @@ -689,6 +697,10 @@ private extension MainTimelineModernViewController {
alert.addAction(action)
}

if let action = self.markAllAsReadExceptStarredAlertAction(indexPath, completion: completion) {
alert.addAction(action)
}

if let action = self.openInBrowserAlertAction(article, completion: completion) {
alert.addAction(action)
}
Expand Down Expand Up @@ -1239,6 +1251,45 @@ extension MainTimelineModernViewController {
return action
}

func markAllAsReadExceptStarredAction(_ indexPath: IndexPath) -> UIAction? {
guard coordinator?.canMarkAllAsReadExceptStarred() ?? false else {
return nil
}
guard let collectionView, let contentView = collectionView.cellForItem(at: indexPath)?.contentView else {
return nil
}

let title = NSLocalizedString("Mark Unstarred as Read", comment: "Command")
let action = UIAction(title: title, image: Assets.Images.markAllAsRead) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
self?.markAllAsReadExceptStarredInTimeline()
}
}
return action
}

func markAllAsReadExceptStarredAlertAction(_ indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard coordinator?.canMarkAllAsReadExceptStarred() ?? false else {
return nil
}
guard let collectionView, let contentView = collectionView.cellForItem(at: indexPath)?.contentView else {
return nil
}

let title = NSLocalizedString("Mark Unstarred as Read", comment: "Command")
let cancel = {
completion(true)
}

let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
self?.markAllAsReadExceptStarredInTimeline()
completion(true)
}
}
return action
}

func copyArticleURLAction(_ article: Article) -> UIAction? {
guard let url = article.preferredURL else { return nil }
let title = NSLocalizedString("Copy Article URL", comment: "Copy Article URL")
Expand Down
Loading