From b2d3128b2d75a96dfecf0e794b811d728aea13f6 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Tue, 19 Mar 2024 10:15:30 -0700 Subject: [PATCH] Fix build errors. --- Account/Sources/Account/Account.swift | 8 +- .../ArticlesDatabase/ArticlesDatabase.swift | 2 +- Mac/AppDelegate.swift | 22 --- NetNewsWire.xcodeproj/project.pbxproj | 8 + Shared/Activity/ActivityManager.swift | 56 ++++--- Shared/ArticlePathInfo.swift | 31 ++++ Shared/Widget/WidgetDataEncoder.swift | 150 +++++++++--------- iOS/AppDelegate.swift | 90 ++++++----- iOS/Feeds/SidebarViewController.swift | 31 +++- iOS/SceneCoordinator.swift | 4 +- iOS/Timeline/TimelineViewController.swift | 38 +++-- 11 files changed, 256 insertions(+), 184 deletions(-) create mode 100644 Shared/ArticlePathInfo.swift diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 37c3b6cb5..bcbf97146 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -456,7 +456,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func suspendDatabase() { #if os(iOS) - database.cancelAndSuspend() + Task { + await database.suspend() + } #endif save() } @@ -465,7 +467,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, /// Call this *before* calling resume. public func resumeDatabaseAndDelegate() { #if os(iOS) - database.resume() + Task { + await database.resume() + } #endif delegate.resume() } diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift index c8b92da35..b1018dde1 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift @@ -282,7 +282,7 @@ public actor ArticlesDatabase { #endif } - func resume() { + public func resume() { #if os(iOS) if database == nil { self.database = FMDatabase.openAndSetUpDatabase(path: databasePath) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 92446ad74..94fa565d4 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -968,28 +968,6 @@ extension AppDelegate: NSWindowRestoration { private extension AppDelegate { - struct ArticlePathInfo { - - let accountID: String - let articleID: String - - init?(userInfo: [AnyHashable: Any]) { - - guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [String: String] else { - return nil - } - guard let accountID = articlePathUserInfo[ArticlePathKey.accountID] else { - return nil - } - guard let articleID = articlePathUserInfo[ArticlePathKey.articleID] else { - return nil - } - - self.accountID = accountID - self.articleID = articleID - } - } - func handleMarkAsRead(userInfo: [AnyHashable: Any]) { guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else { diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index f9508aecd..18db61477 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -632,6 +632,9 @@ 842E45DD1ED8C54B000A8B52 /* Browser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45DC1ED8C54B000A8B52 /* Browser.swift */; }; 84411E711FE5FBFA004B527F /* SmallIconProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */; }; 8444C8F21FED81840051386C /* OPMLExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8444C8F11FED81840051386C /* OPMLExporter.swift */; }; + 844933D22BA953590068AC51 /* ArticlePathInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844933D12BA953590068AC51 /* ArticlePathInfo.swift */; }; + 844933D32BA953590068AC51 /* ArticlePathInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844933D12BA953590068AC51 /* ArticlePathInfo.swift */; }; + 844933D42BA953590068AC51 /* ArticlePathInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844933D12BA953590068AC51 /* ArticlePathInfo.swift */; }; 844B5B591FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B5B581FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift */; }; 844B5B5B1FEA00FB00C7C76A /* TimelineKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B5B5A1FEA00FB00C7C76A /* TimelineKeyboardDelegate.swift */; }; 844B5B651FEA11F200C7C76A /* GlobalKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844B5B641FEA11F200C7C76A /* GlobalKeyboardShortcuts.plist */; }; @@ -1341,6 +1344,7 @@ 842E45DC1ED8C54B000A8B52 /* Browser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Browser.swift; sourceTree = ""; }; 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallIconProvider.swift; sourceTree = ""; }; 8444C8F11FED81840051386C /* OPMLExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLExporter.swift; sourceTree = ""; }; + 844933D12BA953590068AC51 /* ArticlePathInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlePathInfo.swift; sourceTree = ""; }; 844B5B581FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarKeyboardDelegate.swift; sourceTree = ""; }; 844B5B5A1FEA00FB00C7C76A /* TimelineKeyboardDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineKeyboardDelegate.swift; sourceTree = ""; }; 844B5B641FEA11F200C7C76A /* GlobalKeyboardShortcuts.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = GlobalKeyboardShortcuts.plist; sourceTree = ""; }; @@ -2445,6 +2449,7 @@ 842E45CD1ED8C308000A8B52 /* AppNotifications.swift */, 51C4CFEF24D37D1F00AF9874 /* Secrets.swift */, 511B9805237DCAC90028BCAA /* UserInfoKey.swift */, + 844933D12BA953590068AC51 /* ArticlePathInfo.swift */, 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */, 845122752B8CEA9B00480DB0 /* SidebarItem */, 51C452AD2265102800C03939 /* Timeline */, @@ -3802,6 +3807,7 @@ 65ED3FFB235DEF6C0081F399 /* AccountsReaderAPIWindowController.swift in Sources */, 65ED3FFC235DEF6C0081F399 /* AccountsAddLocalWindowController.swift in Sources */, 65ED3FFD235DEF6C0081F399 /* PasteboardFolder.swift in Sources */, + 844933D32BA953590068AC51 /* ArticlePathInfo.swift in Sources */, 51386A8F25673277005F3762 /* AccountCell.swift in Sources */, 65ED3FFE235DEF6C0081F399 /* AccountsFeedbinWindowController.swift in Sources */, 65ED3FFF235DEF6C0081F399 /* SidebarOutlineDataSource.swift in Sources */, @@ -4039,6 +4045,7 @@ B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */, C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */, 17D7586F2679C21800B17787 /* OnePasswordExtension.m in Sources */, + 844933D42BA953590068AC51 /* ArticlePathInfo.swift in Sources */, 17071EF126F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, 84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */, @@ -4139,6 +4146,7 @@ 51BC4AFF247277E0000A6ED8 /* URL-Extensions.swift in Sources */, 849A978A1ED9ECEF007D329B /* ArticleThemesManager.swift in Sources */, 8405DD8A2213E0E3008CE1BF /* DetailContainerView.swift in Sources */, + 844933D22BA953590068AC51 /* ArticlePathInfo.swift in Sources */, 519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */, FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */, 84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */, diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index de78967ef..a3a90ea80 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -7,7 +7,6 @@ // import Foundation -import CoreSpotlight import CoreServices import RSCore import Account @@ -15,6 +14,12 @@ import Articles import Intents import UniformTypeIdentifiers +#if os(iOS) +@preconcurrency import CoreSpotlight +#else +import CoreSpotlight +#endif + class ActivityManager { private var nextUnreadActivity: NSUserActivity? @@ -109,34 +114,45 @@ class ActivityManager { #if os(iOS) static func cleanUp(_ account: Account) { - var ids = [String]() - - if let folders = account.folders { - for folder in folders { - ids.append(identifier(for: folder)) + + Task { @MainActor in + var ids = [String]() + + if let folders = account.folders { + for folder in folders { + ids.append(identifier(for: folder)) + } } + + for feed in account.flattenedFeeds() { + let feedIdentifiers = await identifiers(for: feed) + ids.append(contentsOf: feedIdentifiers) + } + + try? await CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids) } - - for feed in account.flattenedFeeds() { - ids.append(contentsOf: identifiers(for: feed)) - } - - CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids) } static func cleanUp(_ folder: Folder) { - var ids = [String]() - ids.append(identifier(for: folder)) - - for feed in folder.flattenedFeeds() { - ids.append(contentsOf: identifiers(for: feed)) + + Task { @MainActor in + var ids = [String]() + ids.append(identifier(for: folder)) + + for feed in folder.flattenedFeeds() { + let feedIdentifiers = await identifiers(for: feed) + ids.append(contentsOf: feedIdentifiers) + } + + try? await CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids) } - - CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids) } static func cleanUp(_ feed: Feed) { - CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifiers(for: feed)) + Task { @MainActor in + let feedIdentifiers = await identifiers(for: feed) + try? await CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: feedIdentifiers) + } } #endif diff --git a/Shared/ArticlePathInfo.swift b/Shared/ArticlePathInfo.swift new file mode 100644 index 000000000..c1b5e0fb3 --- /dev/null +++ b/Shared/ArticlePathInfo.swift @@ -0,0 +1,31 @@ +// +// ArticlePathInfo.swift +// NetNewsWire +// +// Created by Brent Simmons on 3/18/24. +// Copyright © 2024 Ranchero Software. All rights reserved. +// + +import Foundation + +struct ArticlePathInfo { + + let accountID: String + let articleID: String + + init?(userInfo: [AnyHashable: Any]) { + + guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [String: String] else { + return nil + } + guard let accountID = articlePathUserInfo[ArticlePathKey.accountID] else { + return nil + } + guard let articleID = articlePathUserInfo[ArticlePathKey.articleID] else { + return nil + } + + self.accountID = accountID + self.articleID = articleID + } +} diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 2b8fb18d7..fe88389b7 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -31,81 +31,81 @@ public final class WidgetDataEncoder { func encodeWidgetData() throws { os_log(.debug, log: log, "Starting encoding widget data.") - do { - let unreadArticles = Array(try AccountManager.shared.fetchArticles(.unread(fetchLimit))).sortedByDate(.orderedDescending) - let starredArticles = Array(try AccountManager.shared.fetchArticles(.starred(fetchLimit))).sortedByDate(.orderedDescending) - let todayArticles = Array(try AccountManager.shared.fetchArticles(.today(fetchLimit))).sortedByDate(.orderedDescending) - - var unread = [LatestArticle]() - var today = [LatestArticle]() - var starred = [LatestArticle]() - - for article in unreadArticles { - let latestArticle = LatestArticle(id: article.sortableArticleID, - feedTitle: article.sortableName, - articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), - articleSummary: article.summary, - feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished?.description ?? "") - unread.append(latestArticle) - } - - for article in starredArticles { - let latestArticle = LatestArticle(id: article.sortableArticleID, - feedTitle: article.sortableName, - articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), - articleSummary: article.summary, - feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished?.description ?? "") - starred.append(latestArticle) - } - - for article in todayArticles { - let latestArticle = LatestArticle(id: article.sortableArticleID, - feedTitle: article.sortableName, - articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), - articleSummary: article.summary, - feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished?.description ?? "") - today.append(latestArticle) - } - - let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount, - currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount, - currentStarredCount: try! SmartFeedsController.shared.starredFeed.fetchArticles().count, - unreadArticles: unread, - starredArticles: starred, - todayArticles:today, - lastUpdateTime: Date()) - - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - - self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "com.ranchero.NetNewsWire.Encode") { - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid - } - let encodedData = try? JSONEncoder().encode(latestData) - - os_log(.debug, log: self.log, "Finished encoding widget data.") - - if self.fileExists() { - try? FileManager.default.removeItem(at: self.dataURL!) - os_log(.debug, log: self.log, "Removed widget data from container.") - } - if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) { - os_log(.debug, log: self.log, "Wrote widget data to container.") - WidgetCenter.shared.reloadAllTimelines() - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid - } else { - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid - } - - } - } +// do { +// let unreadArticles = Array(try AccountManager.shared.fetchArticles(fetchType: .unread(fetchLimit))).sortedByDate(.orderedDescending) +// let starredArticles = Array(try AccountManager.shared.fetchArticles(fetchType: .starred(fetchLimit))).sortedByDate(.orderedDescending) +// let todayArticles = Array(try AccountManager.shared.fetchArticles(fetchType: .today(fetchLimit))).sortedByDate(.orderedDescending) +// +// var unread = [LatestArticle]() +// var today = [LatestArticle]() +// var starred = [LatestArticle]() +// +// for article in unreadArticles { +// let latestArticle = LatestArticle(id: article.sortableArticleID, +// feedTitle: article.sortableName, +// articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), +// articleSummary: article.summary, +// feedIcon: article.iconImage()?.image.dataRepresentation(), +// pubDate: article.datePublished?.description ?? "") +// unread.append(latestArticle) +// } +// +// for article in starredArticles { +// let latestArticle = LatestArticle(id: article.sortableArticleID, +// feedTitle: article.sortableName, +// articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), +// articleSummary: article.summary, +// feedIcon: article.iconImage()?.image.dataRepresentation(), +// pubDate: article.datePublished?.description ?? "") +// starred.append(latestArticle) +// } +// +// for article in todayArticles { +// let latestArticle = LatestArticle(id: article.sortableArticleID, +// feedTitle: article.sortableName, +// articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), +// articleSummary: article.summary, +// feedIcon: article.iconImage()?.image.dataRepresentation(), +// pubDate: article.datePublished?.description ?? "") +// today.append(latestArticle) +// } +// +// let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount, +// currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount, +// currentStarredCount: try! SmartFeedsController.shared.starredFeed.fetchArticles().count, +// unreadArticles: unread, +// starredArticles: starred, +// todayArticles:today, +// lastUpdateTime: Date()) +// +// +// DispatchQueue.global().async { [weak self] in +// guard let self = self else { return } +// +// self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "com.ranchero.NetNewsWire.Encode") { +// UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) +// self.backgroundTaskID = .invalid +// } +// let encodedData = try? JSONEncoder().encode(latestData) +// +// os_log(.debug, log: self.log, "Finished encoding widget data.") +// +// if self.fileExists() { +// try? FileManager.default.removeItem(at: self.dataURL!) +// os_log(.debug, log: self.log, "Removed widget data from container.") +// } +// if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) { +// os_log(.debug, log: self.log, "Wrote widget data to container.") +// WidgetCenter.shared.reloadAllTimelines() +// UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) +// self.backgroundTaskID = .invalid +// } else { +// UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) +// self.backgroundTaskID = .invalid +// } +// +// } +// } } private func fileExists() -> Bool { diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 0ab401f19..f584fbef1 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -421,58 +421,70 @@ private extension AppDelegate { private extension AppDelegate { - func handleMarkAsRead(userInfo: [AnyHashable: Any]) { - guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], - let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, - let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { - return + @MainActor func handleMarkAsRead(userInfo: [AnyHashable: Any]) { + + guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else { + return } + resumeDatabaseProcessingIfNecessary() - let account = AccountManager.shared.existingAccount(with: accountID) - guard account != nil else { + + guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else { os_log(.debug, "No account found from notification.") return } - let article = try? account!.fetchArticles(.articleIDs([articleID])) - guard article != nil else { - os_log(.debug, "No article found from search using %@", articleID) - return - } - account!.markArticles(article!, statusKey: .read, flag: true) { _ in } - self.prepareAccountsForBackground() - account!.syncArticleStatus(completion: { [weak self] _ in - if !AccountManager.shared.isSuspended { - try? WidgetDataEncoder.shared.encodeWidgetData() - self?.prepareAccountsForBackground() - self?.suspendApplication() + let articleID = articlePathInfo.articleID + + Task { @MainActor in + guard let articles = try? await account.articles(for: .articleIDs([articleID])) else { + os_log(.debug, "No article found from search using %@", articleID) + return } - }) + + account.markArticles(articles, statusKey: .read, flag: true) { _ in } + + self.prepareAccountsForBackground() + + account.syncArticleStatus(completion: { [weak self] _ in + if !AccountManager.shared.isSuspended { + try? WidgetDataEncoder.shared.encodeWidgetData() + self?.prepareAccountsForBackground() + self?.suspendApplication() + } + }) + } } - func handleMarkAsStarred(userInfo: [AnyHashable: Any]) { - guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], - let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, - let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { - return + @MainActor func handleMarkAsStarred(userInfo: [AnyHashable: Any]) { + + guard let articlePathInfo = ArticlePathInfo(userInfo: userInfo) else { + return } + resumeDatabaseProcessingIfNecessary() - let account = AccountManager.shared.existingAccount(with: accountID) - guard account != nil else { + + guard let account = AccountManager.shared.existingAccount(with: articlePathInfo.accountID) else { os_log(.debug, "No account found from notification.") return } - let article = try? account!.fetchArticles(.articleIDs([articleID])) - guard article != nil else { - os_log(.debug, "No article found from search using %@", articleID) - return - } - account!.markArticles(article!, statusKey: .starred, flag: true) { _ in } - account!.syncArticleStatus(completion: { [weak self] _ in - if !AccountManager.shared.isSuspended { - try? WidgetDataEncoder.shared.encodeWidgetData() - self?.prepareAccountsForBackground() - self?.suspendApplication() + let articleID = articlePathInfo.articleID + + Task { @MainActor in + + guard let articles = try? await account.articles(for: .articleIDs([articleID])) else { + os_log(.debug, "No article found from search using %@", articleID) + return } - }) + + account.markArticles(articles, statusKey: .starred, flag: true) { _ in } + + account.syncArticleStatus(completion: { [weak self] _ in + if !AccountManager.shared.isSuspended { + try? WidgetDataEncoder.shared.encodeWidgetData() + self?.prepareAccountsForBackground() + self?.suspendApplication() + } + }) + } } } diff --git a/iOS/Feeds/SidebarViewController.swift b/iOS/Feeds/SidebarViewController.swift index 8eb919da4..70d6ed31e 100644 --- a/iOS/Feeds/SidebarViewController.swift +++ b/iOS/Feeds/SidebarViewController.swift @@ -986,6 +986,7 @@ private extension SidebarViewController { } func copyHomePageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { + guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed, let homePageURL = feed.homePageURL, let url = URL(string: homePageURL) else { @@ -1001,9 +1002,12 @@ private extension SidebarViewController { } func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { + guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed, - feed.unreadCount > 0, - let articles = try? feed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { + feed.unreadCount > 0 else { + return nil + } + guard let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } @@ -1016,8 +1020,13 @@ private extension SidebarViewController { let action = UIAlertAction(title: title, style: .default) { [weak self] action in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in - self?.coordinator.markAllAsRead(Array(articles)) - completion(true) + + Task { @MainActor in + if let articles = try? await feed.fetchUnreadArticles() { + self?.coordinator.markAllAsRead(Array(articles)) + } + completion(true) + } } } return action @@ -1092,8 +1101,11 @@ private extension SidebarViewController { let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in - if let articles = try? feed.fetchUnreadArticles() { - self?.coordinator.markAllAsRead(Array(articles)) + + Task { @MainActor in + if let articles = try? await feed.fetchUnreadArticles() { + self?.coordinator.markAllAsRead(Array(articles)) + } } } } @@ -1112,8 +1124,11 @@ private extension SidebarViewController { MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, 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) { - if let articles = try? account.fetchArticles(.unread()) { - self?.coordinator.markAllAsRead(Array(articles)) + + Task { @MainActor in + if let articles = try? await account.articles(for: .unread()) { + self?.coordinator.markAllAsRead(Array(articles)) + } } } } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 818b5e325..f6394a824 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -51,8 +51,8 @@ struct SidebarItemNode: Hashable { } } -class SceneCoordinator: NSObject, UndoableCommandRunner { - +@MainActor final class SceneCoordinator: NSObject, UndoableCommandRunner { + var undoableCommands = [UndoableCommand]() var undoManager: UndoManager? { return rootSplitViewController.undoManager diff --git a/iOS/Timeline/TimelineViewController.swift b/iOS/Timeline/TimelineViewController.swift index 86faafbd1..0933198e8 100644 --- a/iOS/Timeline/TimelineViewController.swift +++ b/iOS/Timeline/TimelineViewController.swift @@ -867,36 +867,37 @@ private extension TimelineViewController { } func markAllInFeedAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? { - guard let feed = article.feed else { return nil } - guard let fetchedArticles = try? feed.fetchArticles() else { - return nil - } - let articles = Array(fetchedArticles) - guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { + guard let feed = article.feed, feed.unreadCount > 0 else { + return nil + } + guard let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } - let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in - self?.coordinator.markAllAsRead(articles) + + Task { @MainActor in + if let articles = try? await feed.fetchArticles() { + self?.coordinator.markAllAsRead(Array(articles)) + } + } + } } return action } func markAllInFeedAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { - guard let feed = article.feed else { return nil } - guard let fetchedArticles = try? feed.fetchArticles() else { + + guard let feed = article.feed, feed.unreadCount > 0 else { return nil } - - let articles = Array(fetchedArticles) - guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { + guard let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } @@ -908,8 +909,15 @@ private extension TimelineViewController { let action = UIAlertAction(title: title, style: .default) { [weak self] action in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in - self?.coordinator.markAllAsRead(articles) - completion(true) + + Task { @MainActor in + if let articles = try? await feed.fetchArticles() { + self?.coordinator.markAllAsRead(Array(articles)) + completion(true) + } else { + completion(false) + } + } } } return action