Skip to content

Commit c52828e

Browse files
committed
Add Mark Unstarred as Read command
1 parent c6d9280 commit c52828e

5 files changed

Lines changed: 149 additions & 1 deletion

File tree

Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,19 @@ extension SidebarViewController {
7676
runCommand(markReadCommand)
7777
}
7878

79+
@objc func markObjectsReadExceptStarredFromContextualMenu(_ sender: Any?) {
80+
81+
guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [Any] else {
82+
return
83+
}
84+
85+
let articles = unreadUnstarredArticles(for: objects)
86+
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else {
87+
return
88+
}
89+
runCommand(markReadCommand)
90+
}
91+
7992
@objc func deleteFromContextualMenu(_ sender: Any?) {
8093
guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [AnyObject] else {
8194
return
@@ -212,6 +225,9 @@ private extension SidebarViewController {
212225

213226
if feed.unreadCount > 0 {
214227
menu.addItem(markAllReadMenuItem([feed]))
228+
if anyObjectHasUnreadUnstarredArticles([feed]) {
229+
menu.addItem(markAllAsReadExceptStarredMenuItem([feed]))
230+
}
215231
menu.addItem(NSMenuItem.separator())
216232
}
217233

@@ -264,6 +280,9 @@ private extension SidebarViewController {
264280

265281
if folder.unreadCount > 0 {
266282
menu.addItem(markAllReadMenuItem([folder]))
283+
if anyObjectHasUnreadUnstarredArticles([folder]) {
284+
menu.addItem(markAllAsReadExceptStarredMenuItem([folder]))
285+
}
267286
menu.addItem(NSMenuItem.separator())
268287
}
269288

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

280299
if smartFeed.unreadCount > 0 {
281300
menu.addItem(markAllReadMenuItem([smartFeed]))
301+
if anyObjectHasUnreadUnstarredArticles([smartFeed]) {
302+
menu.addItem(markAllAsReadExceptStarredMenuItem([smartFeed]))
303+
}
282304
}
283305
return menu.numberOfItems > 0 ? menu : nil
284306
}
@@ -289,6 +311,9 @@ private extension SidebarViewController {
289311

290312
if anyObjectInArrayHasNonZeroUnreadCount(objects) {
291313
menu.addItem(markAllReadMenuItem(objects))
314+
if anyObjectHasUnreadUnstarredArticles(objects) {
315+
menu.addItem(markAllAsReadExceptStarredMenuItem(objects))
316+
}
292317
}
293318

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

332+
func markAllAsReadExceptStarredMenuItem(_ objects: [Any]) -> NSMenuItem {
333+
334+
return menuItem(NSLocalizedString("Mark Unstarred as Read", comment: "Command"), #selector(markObjectsReadExceptStarredFromContextualMenu(_:)), objects, image: Assets.Images.markAllAsReadMenu)
335+
}
336+
307337
func deleteMenuItem(_ objects: [Any]) -> NSMenuItem {
308338

309339
return menuItem(NSLocalizedString("Delete", comment: "Command"), #selector(deleteFromContextualMenu(_:)), objects, image: Assets.Images.delete)
@@ -364,4 +394,32 @@ private extension SidebarViewController {
364394
}
365395
return articles
366396
}
397+
398+
func unreadUnstarredArticles(for objects: [Any]) -> Set<Article> {
399+
400+
var articles = Set<Article>()
401+
for object in objects {
402+
if let articleFetcher = object as? ArticleFetcher {
403+
if let unreadArticles = try? articleFetcher.fetchUnreadArticles() {
404+
let unstarred = unreadArticles.filter { !$0.status.starred }
405+
articles.formUnion(unstarred)
406+
}
407+
}
408+
}
409+
return articles
410+
}
411+
412+
func anyObjectHasUnreadUnstarredArticles(_ objects: [Any]) -> Bool {
413+
414+
for object in objects {
415+
if let articleFetcher = object as? ArticleFetcher {
416+
if let unreadArticles = try? articleFetcher.fetchUnreadArticles() {
417+
if unreadArticles.contains(where: { !$0.status.starred }) {
418+
return true
419+
}
420+
}
421+
}
422+
}
423+
return false
424+
}
367425
}

Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ extension TimelineViewController {
5252
markBelowArticlesRead(articles)
5353
}
5454

55+
@objc func markAllAsReadExceptStarredFromContextualMenu(_ sender: Any?) {
56+
guard let unreadUnstarred = self.articles.unreadUnstarredArticles() else {
57+
return
58+
}
59+
markArticles(unreadUnstarred, read: true)
60+
}
61+
5562
@objc func markArticlesStarredFromContextualMenu(_ sender: Any?) {
5663
guard let articles = articles(from: sender) else { return }
5764
markArticles(articles, starred: true)
@@ -167,6 +174,9 @@ private extension TimelineViewController {
167174
if let last = articles.last, self.articles.articlesBelow(article: last).canMarkAllAsRead() {
168175
menu.addItem(markBelowReadMenuItem(articles))
169176
}
177+
if self.articles.canMarkAllAsReadExceptStarred() {
178+
menu.addItem(markAllAsReadExceptStarredMenuItem(articles))
179+
}
170180

171181
menu.addSeparatorIfNeeded()
172182

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

265+
func markAllAsReadExceptStarredMenuItem(_ articles: [Article]) -> NSMenuItem {
266+
return menuItem(NSLocalizedString("Mark Unstarred as Read", comment: "Command"), #selector(markAllAsReadExceptStarredFromContextualMenu(_:)), articles, image: Assets.Images.markAllAsReadMenu)
267+
}
268+
255269
func selectFeedInSidebarMenuItem(_ feed: Feed) -> NSMenuItem {
256270
let localizedMenuText = NSLocalizedString("Select “%@” in Sidebar", comment: "Command")
257271
let formattedMenuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay)

Shared/Timeline/ArticleArray.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ typealias ArticleArray = [Article]
9292
return articles.isEmpty ? nil : articles
9393
}
9494

95+
func canMarkAllAsReadExceptStarred() -> Bool {
96+
return anyArticlePassesTest { !$0.status.read && !$0.status.starred }
97+
}
98+
99+
func unreadUnstarredArticles() -> [Article]? {
100+
let articles = self.filter { !$0.status.read && !$0.status.starred }
101+
return articles.isEmpty ? nil : articles
102+
}
103+
95104
func representSameArticlesInSameOrder(as otherArticles: [Article]) -> Bool {
96105
if self.count != otherArticles.count {
97106
return false

iOS/MainTimeline/MainTimelineModernViewController.swift

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,11 @@ final class MainTimelineModernViewController: UIViewController, UndoableCommandR
436436
coordinator?.markAllAsReadInTimeline()
437437
}
438438

439+
private func markAllAsReadExceptStarredInTimeline() {
440+
assert(coordinator != nil)
441+
coordinator?.markAllAsReadExceptStarredInTimeline()
442+
}
443+
439444
@IBAction func markAllAsRead(_ sender: Any?) {
440445
let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
441446

@@ -516,6 +521,9 @@ extension MainTimelineModernViewController: UICollectionViewDelegate {
516521
if let action = self.markAllInFeedAsReadAction(article, indexPath: firstIndex) {
517522
secondaryActions.append(action)
518523
}
524+
if let action = self.markAllAsReadExceptStarredAction(firstIndex) {
525+
secondaryActions.append(action)
526+
}
519527
if !secondaryActions.isEmpty {
520528
menuElements.append(UIMenu(title: "", options: .displayInline, children: secondaryActions))
521529
}
@@ -689,6 +697,10 @@ private extension MainTimelineModernViewController {
689697
alert.addAction(action)
690698
}
691699

700+
if let action = self.markAllAsReadExceptStarredAlertAction(indexPath, completion: completion) {
701+
alert.addAction(action)
702+
}
703+
692704
if let action = self.openInBrowserAlertAction(article, completion: completion) {
693705
alert.addAction(action)
694706
}
@@ -1224,7 +1236,7 @@ extension MainTimelineModernViewController {
12241236
return nil
12251237
}
12261238

1227-
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Mark All as Read in Feed")
1239+
let localizedMenuText = NSLocalizedString("Mark All as Read in "%@"", comment: "Mark All as Read in Feed")
12281240
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
12291241
let cancel = {
12301242
completion(true)
@@ -1239,6 +1251,45 @@ extension MainTimelineModernViewController {
12391251
return action
12401252
}
12411253

1254+
func markAllAsReadExceptStarredAction(_ indexPath: IndexPath) -> UIAction? {
1255+
guard coordinator?.canMarkAllAsReadExceptStarred() ?? false else {
1256+
return nil
1257+
}
1258+
guard let collectionView, let contentView = collectionView.cellForItem(at: indexPath)?.contentView else {
1259+
return nil
1260+
}
1261+
1262+
let title = NSLocalizedString("Mark Unstarred as Read", comment: "Command")
1263+
let action = UIAction(title: title, image: Assets.Images.markAllAsRead) { [weak self] _ in
1264+
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
1265+
self?.markAllAsReadExceptStarredInTimeline()
1266+
}
1267+
}
1268+
return action
1269+
}
1270+
1271+
func markAllAsReadExceptStarredAlertAction(_ indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
1272+
guard coordinator?.canMarkAllAsReadExceptStarred() ?? false else {
1273+
return nil
1274+
}
1275+
guard let collectionView, let contentView = collectionView.cellForItem(at: indexPath)?.contentView else {
1276+
return nil
1277+
}
1278+
1279+
let title = NSLocalizedString("Mark Unstarred as Read", comment: "Command")
1280+
let cancel = {
1281+
completion(true)
1282+
}
1283+
1284+
let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
1285+
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
1286+
self?.markAllAsReadExceptStarredInTimeline()
1287+
completion(true)
1288+
}
1289+
}
1290+
return action
1291+
}
1292+
12421293
func copyArticleURLAction(_ article: Article) -> UIAction? {
12431294
guard let url = article.preferredURL else { return nil }
12441295
let title = NSLocalizedString("Copy Article URL", comment: "Copy Article URL")

iOS/SceneCoordinator.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,22 @@ struct SidebarItemNode: Hashable, Sendable {
11951195
}
11961196
}
11971197

1198+
func canMarkAllAsReadExceptStarred() -> Bool {
1199+
return articles.canMarkAllAsReadExceptStarred()
1200+
}
1201+
1202+
func markAllAsReadExceptStarredInTimeline(completion: (() -> Void)? = nil) {
1203+
guard let unreadUnstarred = articles.unreadUnstarredArticles() else {
1204+
completion?()
1205+
return
1206+
}
1207+
markAllAsRead(unreadUnstarred) {
1208+
self.rootSplitViewController.preferredDisplayMode = .twoBesideSecondary
1209+
self.rootSplitViewController.show(.primary)
1210+
completion?()
1211+
}
1212+
}
1213+
11981214
func canMarkAboveAsRead(for article: Article) -> Bool {
11991215
let articlesAboveArray = articles.articlesAbove(article: article)
12001216
return articlesAboveArray.canMarkAllAsRead()

0 commit comments

Comments
 (0)