From 709bafbe6229f2aa94c1c46f923f473e8b58899d Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 23 Dec 2020 19:57:33 +0800 Subject: [PATCH 1/9] adds icons and actions to notifications --- .../UserNotificationManager.swift | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift index 6ddbad025..583942169 100644 --- a/Shared/UserNotifications/UserNotificationManager.swift +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -17,6 +17,7 @@ final class UserNotificationManager: NSObject { super.init() NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil) + registerCategoriesAndActions() } @objc func accountDidDownloadArticles(_ note: Notification) { @@ -43,26 +44,63 @@ final class UserNotificationManager: NSObject { private extension UserNotificationManager { - private func sendNotification(webFeed: WebFeed, article: Article) { + func sendNotification(webFeed: WebFeed, article: Article) { let content = UNMutableNotificationContent() content.title = webFeed.nameForDisplay - if !ArticleStringFormatter.truncatedTitle(article).isEmpty { content.subtitle = ArticleStringFormatter.truncatedTitle(article) } - content.body = ArticleStringFormatter.truncatedSummary(article) - content.threadIdentifier = webFeed.webFeedID content.summaryArgument = "\(webFeed.nameForDisplay)" content.summaryArgumentCount = 1 - content.sound = UNNotificationSound.default content.userInfo = [UserInfoKey.articlePath: article.pathUserInfo] + if let attachment = thumbnailAttachment(for: article, webFeed: webFeed) { + content.attachments.append(attachment) + } let request = UNNotificationRequest.init(identifier: "articleID:\(article.articleID)", content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } + /// Determine if there is an available icon for the article. This will then move it to the caches directory and make it avialble for the notification. + /// - Parameters: + /// - article: `Article` + /// - webFeed: `WebFeed` + /// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil. + /// - Warning: In certain scenarios, this will return the `faviconTemplateImage`. + func thumbnailAttachment(for article: Article, webFeed: WebFeed) -> UNNotificationAttachment? { + if let image = article.iconImage() { + let fm = FileManager.default + var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0] + #if os(macOS) + path.appendPathComponent(webFeed.webFeedID + "_smallIcon.tiff") + #else + path.appendPathComponent(webFeed.webFeedID + "_smallIcon.png") + #endif + fm.createFile(atPath: path.path, contents: image.image.dataRepresentation()!, attributes: nil) + + let thumbnail = try? UNNotificationAttachment(identifier: webFeed.webFeedID, url: path, options: nil) + return thumbnail + } + return nil + } + + func registerCategoriesAndActions() { + let readAction = UNNotificationAction(identifier: "MARK_AS_READ", title: NSLocalizedString("Mark as Read", comment: "Mark as Read"), options: []) + let starredAction = UNNotificationAction(identifier: "MARK_AS_STARRED", title: NSLocalizedString("Mark as Starred", comment: "Mark as Starred"), options: []) + let openAction = UNNotificationAction(identifier: "OPEN_ARTICLE", title: NSLocalizedString("Open", comment: "Open"), options: [.foreground]) + + let newArticleCategory = + UNNotificationCategory(identifier: "NEW_ARTICLE_NOTIFICATION_CATEGORY", + actions: [openAction, readAction, starredAction], + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: "", + options: .customDismissAction) + + UNUserNotificationCenter.current().setNotificationCategories([newArticleCategory]) + } + } From 8a05d2f448918b7c3cf59d528788dde95331fe6a Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 23 Dec 2020 20:15:25 +0800 Subject: [PATCH 2/9] handles actions from notifications --- iOS/AppDelegate.swift | 74 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 180536ae5..c5d751a06 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -191,10 +191,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { defer { completionHandler() } - if let sceneDelegate = response.targetScene?.delegate as? SceneDelegate { - sceneDelegate.handle(response) + let userInfo = response.notification.request.content.userInfo + + switch response.actionIdentifier { + case "MARK_AS_READ": + handleMarkAsRead(userInfo: userInfo) + case "MARK_AS_STARRED": + handleMarkAsStarred(userInfo: userInfo) + default: + if let sceneDelegate = response.targetScene?.delegate as? SceneDelegate { + sceneDelegate.handle(response) + } } - + } } @@ -397,3 +406,62 @@ private extension AppDelegate { } } + +// Handle Notification Actions + +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 + } + resumeDatabaseProcessingIfNecessary() + let account = AccountManager.shared.existingAccount(with: accountID) + guard account != nil 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) + self.prepareAccountsForBackground() + if !AccountManager.shared.isSuspended { + if #available(iOS 14, *) { + try? WidgetDataEncoder.shared.encodeWidgetData() + } + 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 + } + resumeDatabaseProcessingIfNecessary() + let account = AccountManager.shared.existingAccount(with: accountID) + guard account != nil 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) + self.prepareAccountsForBackground() + if !AccountManager.shared.isSuspended { + if #available(iOS 14, *) { + try? WidgetDataEncoder.shared.encodeWidgetData() + } + self.suspendApplication() + } + } +} From 6ed662278ad1f0f14889d6181cfba6e051c55a35 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 23 Dec 2020 20:50:41 +0800 Subject: [PATCH 3/9] adds missing packages back to iOS target --- NetNewsWire.xcodeproj/project.pbxproj | 94 +++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 7b851b19d..a213f4085 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -132,6 +132,27 @@ 17D5F17124B0BC6700375168 /* SidebarToolbarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */; }; 17D5F17224B0BC6700375168 /* SidebarToolbarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */; }; 17D5F19524B0C1DD00375168 /* SidebarToolbarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172199F024AB716900A31D04 /* SidebarToolbarModifier.swift */; }; + 17E007FA25936D7B000C23F0 /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 17E007F925936D7B000C23F0 /* RSCore */; }; + 17E007FB25936D7B000C23F0 /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17E007F925936D7B000C23F0 /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 17E0080225936D89000C23F0 /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0080125936D89000C23F0 /* RSCore */; }; + 17E0080325936D89000C23F0 /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17E0080125936D89000C23F0 /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 17E0080F25936DF6000C23F0 /* Articles in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0080E25936DF6000C23F0 /* Articles */; }; + 17E0081025936DF6000C23F0 /* Articles in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17E0080E25936DF6000C23F0 /* Articles */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 17E0081225936DF6000C23F0 /* ArticlesDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0081125936DF6000C23F0 /* ArticlesDatabase */; }; + 17E0081325936DF6000C23F0 /* ArticlesDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17E0081125936DF6000C23F0 /* ArticlesDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 17E0081525936DFF000C23F0 /* Secrets in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0081425936DFF000C23F0 /* Secrets */; }; + 17E0081625936DFF000C23F0 /* Secrets in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17E0081425936DFF000C23F0 /* Secrets */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 17E0081825936DFF000C23F0 /* SyncDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0081725936DFF000C23F0 /* SyncDatabase */; }; + 17E0081925936DFF000C23F0 /* SyncDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17E0081725936DFF000C23F0 /* SyncDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 17E0082025936E31000C23F0 /* Articles in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0081F25936E31000C23F0 /* Articles */; }; + 17E0082125936E31000C23F0 /* Articles in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17E0081F25936E31000C23F0 /* Articles */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 17E0082325936E31000C23F0 /* ArticlesDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0082225936E31000C23F0 /* ArticlesDatabase */; }; + 17E0082425936E31000C23F0 /* ArticlesDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17E0082225936E31000C23F0 /* ArticlesDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 17E0082625936E31000C23F0 /* Secrets in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0082525936E31000C23F0 /* Secrets */; }; + 17E0082725936E31000C23F0 /* Secrets in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17E0082525936E31000C23F0 /* Secrets */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 17E0082925936E31000C23F0 /* SyncDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0082825936E31000C23F0 /* SyncDatabase */; }; + 17E0082A25936E31000C23F0 /* SyncDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17E0082825936E31000C23F0 /* SyncDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 17E0083025936E70000C23F0 /* Account in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 516B695E24D2F33B00B5702F /* Account */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 17E4DBD624BFC53E00FE462A /* AdvancedPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E4DBD524BFC53E00FE462A /* AdvancedPreferencesModel.swift */; }; 3B3A32A5238B820900314204 /* FeedWranglerAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3A328B238B820900314204 /* FeedWranglerAccountViewController.swift */; }; 3B826DCB2385C84800FC1ADB /* AccountsFeedWrangler.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3B826DB02385C84800FC1ADB /* AccountsFeedWrangler.xib */; }; @@ -1211,6 +1232,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 17E007FB25936D7B000C23F0 /* RSCore in Embed Frameworks */, 51BC2F4924D3439E00E90810 /* RSTree in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -1222,6 +1244,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 17E0080325936D89000C23F0 /* RSCore in Embed Frameworks */, 51BC2F4E24D343AB00E90810 /* RSTree in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -1257,11 +1280,16 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 17E0082725936E31000C23F0 /* Secrets in Embed Frameworks */, 5138E95324D3418100AFF0FE /* RSParser in Embed Frameworks */, 5138E94A24D3416D00AFF0FE /* RSCore in Embed Frameworks */, 5138E95924D3419000AFF0FE /* RSWeb in Embed Frameworks */, + 17E0082A25936E31000C23F0 /* SyncDatabase in Embed Frameworks */, + 17E0082125936E31000C23F0 /* Articles in Embed Frameworks */, + 17E0083025936E70000C23F0 /* Account in Embed Frameworks */, 5138E93B24D33E5600AFF0FE /* RSTree in Embed Frameworks */, 5138E94D24D3417A00AFF0FE /* RSDatabase in Embed Frameworks */, + 17E0082425936E31000C23F0 /* ArticlesDatabase in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -1272,11 +1300,15 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 17E0081625936DFF000C23F0 /* Secrets in Embed Frameworks */, 17A1598924E3DEDD005DA32A /* RSParser in Embed Frameworks */, 17A1598624E3DEDD005DA32A /* RSDatabase in Embed Frameworks */, 17A1598324E3DEDD005DA32A /* RSWeb in Embed Frameworks */, + 17E0081925936DFF000C23F0 /* SyncDatabase in Embed Frameworks */, + 17E0081025936DF6000C23F0 /* Articles in Embed Frameworks */, 17A1597D24E3DEDD005DA32A /* RSCore in Embed Frameworks */, 17A1598024E3DEDD005DA32A /* RSTree in Embed Frameworks */, + 17E0081325936DF6000C23F0 /* ArticlesDatabase in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -2025,6 +2057,7 @@ buildActionMask = 2147483647; files = ( 51BC2F4B24D343A500E90810 /* Account in Frameworks */, + 17E0080225936D89000C23F0 /* RSCore in Frameworks */, 51BC2F4D24D343AB00E90810 /* RSTree in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2034,6 +2067,7 @@ buildActionMask = 2147483647; files = ( 51BC2F3824D3439A00E90810 /* Account in Frameworks */, + 17E007FA25936D7B000C23F0 /* RSCore in Frameworks */, 51BC2F4824D3439E00E90810 /* RSTree in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2055,7 +2089,11 @@ 17A1597C24E3DEDD005DA32A /* RSCore in Frameworks */, 516B695D24D2F28E00B5702F /* Account in Frameworks */, 17A1598524E3DEDD005DA32A /* RSDatabase in Frameworks */, + 17E0080F25936DF6000C23F0 /* Articles in Frameworks */, + 17E0081525936DFF000C23F0 /* Secrets in Frameworks */, 51E4989724A8065700B667CB /* CloudKit.framework in Frameworks */, + 17E0081225936DF6000C23F0 /* ArticlesDatabase in Frameworks */, + 17E0081825936DFF000C23F0 /* SyncDatabase in Frameworks */, 51E4989924A8067000B667CB /* WebKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2110,7 +2148,11 @@ 5138E95224D3418100AFF0FE /* RSParser in Frameworks */, 5138E94C24D3417A00AFF0FE /* RSDatabase in Frameworks */, 51C452B42265141B00C03939 /* WebKit.framework in Frameworks */, + 17E0082025936E31000C23F0 /* Articles in Frameworks */, + 17E0082625936E31000C23F0 /* Secrets in Frameworks */, 51E4DB082425F9EB0091EB5B /* CloudKit.framework in Frameworks */, + 17E0082325936E31000C23F0 /* ArticlesDatabase in Frameworks */, + 17E0082925936E31000C23F0 /* SyncDatabase in Frameworks */, 5138E93A24D33E5600AFF0FE /* RSTree in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3700,6 +3742,7 @@ packageProductDependencies = ( 51BC2F4A24D343A500E90810 /* Account */, 51BC2F4C24D343AB00E90810 /* RSTree */, + 17E0080125936D89000C23F0 /* RSCore */, ); productName = "NetNewsWire iOS Intents Extension"; productReference = 51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */; @@ -3722,6 +3765,7 @@ packageProductDependencies = ( 51BC2F3724D3439A00E90810 /* Account */, 51BC2F4724D3439E00E90810 /* RSTree */, + 17E007F925936D7B000C23F0 /* RSCore */, ); productName = "NetNewsWire iOS Share Extension"; productReference = 513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */; @@ -3766,6 +3810,10 @@ 17A1598124E3DEDD005DA32A /* RSWeb */, 17A1598424E3DEDD005DA32A /* RSDatabase */, 17A1598724E3DEDD005DA32A /* RSParser */, + 17E0080E25936DF6000C23F0 /* Articles */, + 17E0081125936DF6000C23F0 /* ArticlesDatabase */, + 17E0081425936DFF000C23F0 /* Secrets */, + 17E0081725936DFF000C23F0 /* SyncDatabase */, ); productName = iOS; productReference = 51C0513D24A77DF800194D5E /* NetNewsWire.app */; @@ -3886,6 +3934,10 @@ 5138E94B24D3417A00AFF0FE /* RSDatabase */, 5138E95124D3418100AFF0FE /* RSParser */, 5138E95724D3419000AFF0FE /* RSWeb */, + 17E0081F25936E31000C23F0 /* Articles */, + 17E0082225936E31000C23F0 /* ArticlesDatabase */, + 17E0082525936E31000C23F0 /* Secrets */, + 17E0082825936E31000C23F0 /* SyncDatabase */, ); productName = "NetNewsWire-iOS"; productReference = 840D617C2029031C009BC708 /* NetNewsWire.app */; @@ -6088,6 +6140,48 @@ package = 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */; productName = RSParser; }; + 17E007F925936D7B000C23F0 /* RSCore */ = { + isa = XCSwiftPackageProductDependency; + package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */; + productName = RSCore; + }; + 17E0080125936D89000C23F0 /* RSCore */ = { + isa = XCSwiftPackageProductDependency; + package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */; + productName = RSCore; + }; + 17E0080E25936DF6000C23F0 /* Articles */ = { + isa = XCSwiftPackageProductDependency; + productName = Articles; + }; + 17E0081125936DF6000C23F0 /* ArticlesDatabase */ = { + isa = XCSwiftPackageProductDependency; + productName = ArticlesDatabase; + }; + 17E0081425936DFF000C23F0 /* Secrets */ = { + isa = XCSwiftPackageProductDependency; + productName = Secrets; + }; + 17E0081725936DFF000C23F0 /* SyncDatabase */ = { + isa = XCSwiftPackageProductDependency; + productName = SyncDatabase; + }; + 17E0081F25936E31000C23F0 /* Articles */ = { + isa = XCSwiftPackageProductDependency; + productName = Articles; + }; + 17E0082225936E31000C23F0 /* ArticlesDatabase */ = { + isa = XCSwiftPackageProductDependency; + productName = ArticlesDatabase; + }; + 17E0082525936E31000C23F0 /* Secrets */ = { + isa = XCSwiftPackageProductDependency; + productName = Secrets; + }; + 17E0082825936E31000C23F0 /* SyncDatabase */ = { + isa = XCSwiftPackageProductDependency; + productName = SyncDatabase; + }; 5102AE6824D17F7C0050839C /* RSCore */ = { isa = XCSwiftPackageProductDependency; package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */; From 3898edc8b1c6b94ea276ff00d7a997bacaf6c341 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 23 Dec 2020 21:16:32 +0800 Subject: [PATCH 4/9] widget text spacing --- Widget/Widget Views/StarredWidget.swift | 2 +- Widget/Widget Views/TodayWidget.swift | 2 +- Widget/Widget Views/UnreadWidget.swift | 2 +- iOS/AppDelegate.swift | 27 +++++++++++++++---------- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Widget/Widget Views/StarredWidget.swift b/Widget/Widget Views/StarredWidget.swift index 919d2819e..945fc041e 100644 --- a/Widget/Widget Views/StarredWidget.swift +++ b/Widget/Widget Views/StarredWidget.swift @@ -23,7 +23,7 @@ struct StarredWidgetView : View { else { GeometryReader { metrics in HStack(alignment: .top, spacing: 4) { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: -4) { starredImage Spacer() Text(L10n.localizedCount(entry.widgetData.currentStarredCount)).bold().font(.callout).minimumScaleFactor(0.5).lineLimit(1) diff --git a/Widget/Widget Views/TodayWidget.swift b/Widget/Widget Views/TodayWidget.swift index 653c35d14..5e1fdd9ab 100644 --- a/Widget/Widget Views/TodayWidget.swift +++ b/Widget/Widget Views/TodayWidget.swift @@ -23,7 +23,7 @@ struct TodayWidgetView : View { else { GeometryReader { metrics in HStack(alignment: .top, spacing: 4) { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: -4) { todayImage Spacer() Text(L10n.localizedCount(entry.widgetData.currentTodayCount)).bold().font(.callout).minimumScaleFactor(0.5).lineLimit(1) diff --git a/Widget/Widget Views/UnreadWidget.swift b/Widget/Widget Views/UnreadWidget.swift index 21f42319d..924be016e 100644 --- a/Widget/Widget Views/UnreadWidget.swift +++ b/Widget/Widget Views/UnreadWidget.swift @@ -23,7 +23,7 @@ struct UnreadWidgetView : View { else { GeometryReader { metrics in HStack(alignment: .top, spacing: 4) { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: -4) { unreadImage Spacer() Text(L10n.localizedCount(entry.widgetData.currentUnreadCount)).bold().font(.callout).minimumScaleFactor(0.5).lineLimit(1) diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index c5d751a06..da8c4dafd 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -430,12 +430,15 @@ private extension AppDelegate { } account!.markArticles(article!, statusKey: .read, flag: true) self.prepareAccountsForBackground() - if !AccountManager.shared.isSuspended { - if #available(iOS 14, *) { - try? WidgetDataEncoder.shared.encodeWidgetData() + account!.syncArticleStatus(completion: { [weak self] _ in + if !AccountManager.shared.isSuspended { + if #available(iOS 14, *) { + try? WidgetDataEncoder.shared.encodeWidgetData() + } + self?.prepareAccountsForBackground() + self?.suspendApplication() } - self.suspendApplication() - } + }) } func handleMarkAsStarred(userInfo: [AnyHashable: Any]) { @@ -456,12 +459,14 @@ private extension AppDelegate { return } account!.markArticles(article!, statusKey: .starred, flag: true) - self.prepareAccountsForBackground() - if !AccountManager.shared.isSuspended { - if #available(iOS 14, *) { - try? WidgetDataEncoder.shared.encodeWidgetData() + account!.syncArticleStatus(completion: { [weak self] _ in + if !AccountManager.shared.isSuspended { + if #available(iOS 14, *) { + try? WidgetDataEncoder.shared.encodeWidgetData() + } + self?.prepareAccountsForBackground() + self?.suspendApplication() } - self.suspendApplication() - } + }) } } From 9a49c6d90654ea4c12fcda4f5ab8c6277a39e0ba Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 23 Dec 2020 21:18:51 +0800 Subject: [PATCH 5/9] add category to notifications --- Shared/UserNotifications/UserNotificationManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift index 583942169..0e040fa3a 100644 --- a/Shared/UserNotifications/UserNotificationManager.swift +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -57,6 +57,7 @@ private extension UserNotificationManager { content.summaryArgumentCount = 1 content.sound = UNNotificationSound.default content.userInfo = [UserInfoKey.articlePath: article.pathUserInfo] + content.categoryIdentifier = "NEW_ARTICLE_NOTIFICATION_CATEGORY" if let attachment = thumbnailAttachment(for: article, webFeed: webFeed) { content.attachments.append(attachment) } From 1d5c433a1aed41c44f17f059c882d9ea261ddb5e Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 23 Dec 2020 21:44:45 +0800 Subject: [PATCH 6/9] adds macOS notification behaviour --- Mac/AppDelegate.swift | 56 ++++++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 4 +- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index dd31e23ac..b5eb7e17e 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -455,7 +455,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - mainWindowController?.handle(response) + + let userInfo = response.notification.request.content.userInfo + + switch response.actionIdentifier { + case "MARK_AS_READ": + handleMarkAsRead(userInfo: userInfo) + case "MARK_AS_STARRED": + handleMarkAsStarred(userInfo: userInfo) + default: + mainWindowController?.handle(response) + } completionHandler() } @@ -791,3 +801,47 @@ extension AppDelegate: NSWindowRestoration { } } + +// Handle Notification Actions + +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 + } + + let account = AccountManager.shared.existingAccount(with: accountID) + guard account != nil 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) + } + + 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 + } + let account = AccountManager.shared.existingAccount(with: accountID) + guard account != nil 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) + } +} diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5d327bbb9..a6fee66e1 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/tid-kijyun/Kanna.git", "state": { "branch": null, - "revision": "4a80ebe93b6966d5083394fcaaaff57a2fcec935", - "version": "5.2.3" + "revision": "c657fb9f5827ef138068215c76ad0bb62bbc92da", + "version": "5.2.4" } }, { From 6ec3d6d667747474c5cbd77e7691eed5fc36d362 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 24 Dec 2020 07:10:40 +0800 Subject: [PATCH 7/9] package.resolved --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a6fee66e1..5d327bbb9 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/tid-kijyun/Kanna.git", "state": { "branch": null, - "revision": "c657fb9f5827ef138068215c76ad0bb62bbc92da", - "version": "5.2.4" + "revision": "4a80ebe93b6966d5083394fcaaaff57a2fcec935", + "version": "5.2.3" } }, { From 3254d57189c478d5c66775d0fdaa50b6c565091d Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 24 Dec 2020 07:23:29 +0800 Subject: [PATCH 8/9] resolve merge commit --- NetNewsWire.xcodeproj/project.pbxproj | 53 --------------------------- 1 file changed, 53 deletions(-) diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 74a709866..0d8021c58 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -1245,11 +1245,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( -<<<<<<< HEAD - 17E007FB25936D7B000C23F0 /* RSCore in Embed Frameworks */, -======= 513F325D2593ECF40003048F /* RSCore in Embed Frameworks */, ->>>>>>> main 51BC2F4924D3439E00E90810 /* RSTree in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -1261,11 +1257,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( -<<<<<<< HEAD - 17E0080325936D89000C23F0 /* RSCore in Embed Frameworks */, -======= 513F32892593EF8F0003048F /* RSCore in Embed Frameworks */, ->>>>>>> main 51BC2F4E24D343AB00E90810 /* RSTree in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -1301,18 +1293,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( -<<<<<<< HEAD - 17E0082725936E31000C23F0 /* Secrets in Embed Frameworks */, - 5138E95324D3418100AFF0FE /* RSParser in Embed Frameworks */, - 5138E94A24D3416D00AFF0FE /* RSCore in Embed Frameworks */, - 5138E95924D3419000AFF0FE /* RSWeb in Embed Frameworks */, - 17E0082A25936E31000C23F0 /* SyncDatabase in Embed Frameworks */, - 17E0082125936E31000C23F0 /* Articles in Embed Frameworks */, - 17E0083025936E70000C23F0 /* Account in Embed Frameworks */, - 5138E93B24D33E5600AFF0FE /* RSTree in Embed Frameworks */, - 5138E94D24D3417A00AFF0FE /* RSDatabase in Embed Frameworks */, - 17E0082425936E31000C23F0 /* ArticlesDatabase in Embed Frameworks */, -======= 513F32782593EE6F0003048F /* Secrets in Embed Frameworks */, 5138E95324D3418100AFF0FE /* RSParser in Embed Frameworks */, 5138E94A24D3416D00AFF0FE /* RSCore in Embed Frameworks */, @@ -1323,7 +1303,6 @@ 5138E93B24D33E5600AFF0FE /* RSTree in Embed Frameworks */, 5138E94D24D3417A00AFF0FE /* RSDatabase in Embed Frameworks */, 513F32752593EE6F0003048F /* ArticlesDatabase in Embed Frameworks */, ->>>>>>> main ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -2090,12 +2069,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 51BC2F4B24D343A500E90810 /* Account in Frameworks */, -<<<<<<< HEAD - 17E0080225936D89000C23F0 /* RSCore in Frameworks */, -======= 513F32882593EF8F0003048F /* RSCore in Frameworks */, ->>>>>>> main 51BC2F4D24D343AB00E90810 /* RSTree in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2105,11 +2079,7 @@ buildActionMask = 2147483647; files = ( 51BC2F3824D3439A00E90810 /* Account in Frameworks */, -<<<<<<< HEAD - 17E007FA25936D7B000C23F0 /* RSCore in Frameworks */, -======= 513F325C2593ECF40003048F /* RSCore in Frameworks */, ->>>>>>> main 51BC2F4824D3439E00E90810 /* RSTree in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2190,19 +2160,11 @@ 5138E95224D3418100AFF0FE /* RSParser in Frameworks */, 5138E94C24D3417A00AFF0FE /* RSDatabase in Frameworks */, 51C452B42265141B00C03939 /* WebKit.framework in Frameworks */, -<<<<<<< HEAD - 17E0082025936E31000C23F0 /* Articles in Frameworks */, - 17E0082625936E31000C23F0 /* Secrets in Frameworks */, - 51E4DB082425F9EB0091EB5B /* CloudKit.framework in Frameworks */, - 17E0082325936E31000C23F0 /* ArticlesDatabase in Frameworks */, - 17E0082925936E31000C23F0 /* SyncDatabase in Frameworks */, -======= 513F32712593EE6F0003048F /* Articles in Frameworks */, 513F32772593EE6F0003048F /* Secrets in Frameworks */, 51E4DB082425F9EB0091EB5B /* CloudKit.framework in Frameworks */, 513F32742593EE6F0003048F /* ArticlesDatabase in Frameworks */, 513F327A2593EE6F0003048F /* SyncDatabase in Frameworks */, ->>>>>>> main 5138E93A24D33E5600AFF0FE /* RSTree in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3793,11 +3755,7 @@ packageProductDependencies = ( 51BC2F4A24D343A500E90810 /* Account */, 51BC2F4C24D343AB00E90810 /* RSTree */, -<<<<<<< HEAD - 17E0080125936D89000C23F0 /* RSCore */, -======= 513F32872593EF8F0003048F /* RSCore */, ->>>>>>> main ); productName = "NetNewsWire iOS Intents Extension"; productReference = 51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */; @@ -3821,11 +3779,7 @@ packageProductDependencies = ( 51BC2F3724D3439A00E90810 /* Account */, 51BC2F4724D3439E00E90810 /* RSTree */, -<<<<<<< HEAD - 17E007F925936D7B000C23F0 /* RSCore */, -======= 513F325B2593ECF40003048F /* RSCore */, ->>>>>>> main ); productName = "NetNewsWire iOS Share Extension"; productReference = 513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */; @@ -3994,17 +3948,10 @@ 5138E94B24D3417A00AFF0FE /* RSDatabase */, 5138E95124D3418100AFF0FE /* RSParser */, 5138E95724D3419000AFF0FE /* RSWeb */, -<<<<<<< HEAD - 17E0081F25936E31000C23F0 /* Articles */, - 17E0082225936E31000C23F0 /* ArticlesDatabase */, - 17E0082525936E31000C23F0 /* Secrets */, - 17E0082825936E31000C23F0 /* SyncDatabase */, -======= 513F32702593EE6F0003048F /* Articles */, 513F32732593EE6F0003048F /* ArticlesDatabase */, 513F32762593EE6F0003048F /* Secrets */, 513F32792593EE6F0003048F /* SyncDatabase */, ->>>>>>> main ); productName = "NetNewsWire-iOS"; productReference = 840D617C2029031C009BC708 /* NetNewsWire.app */; From fe194ec2e5bd7a6ab8c62bfc6f37a680271ac9c6 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 24 Dec 2020 07:31:44 +0800 Subject: [PATCH 9/9] adds iconImageUrl to article utils --- Shared/Extensions/ArticleUtilities.swift | 16 ++++++++++++++++ .../UserNotificationManager.swift | 13 ++----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Shared/Extensions/ArticleUtilities.swift b/Shared/Extensions/ArticleUtilities.swift index aeee931ae..6f6cd5126 100644 --- a/Shared/Extensions/ArticleUtilities.swift +++ b/Shared/Extensions/ArticleUtilities.swift @@ -103,6 +103,22 @@ extension Article { return FaviconGenerator.favicon(webFeed) } + func iconImageUrl(webFeed: WebFeed) -> URL? { + if let image = iconImage() { + let fm = FileManager.default + var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0] + #if os(macOS) + path.appendPathComponent(webFeed.webFeedID + "_smallIcon.tiff") + #else + path.appendPathComponent(webFeed.webFeedID + "_smallIcon.png") + #endif + fm.createFile(atPath: path.path, contents: image.image.dataRepresentation()!, attributes: nil) + return path + } else { + return nil + } + } + func byline() -> String { guard let authors = authors ?? webFeed?.authors, !authors.isEmpty else { return "" diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift index 0e040fa3a..50fe22227 100644 --- a/Shared/UserNotifications/UserNotificationManager.swift +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -73,17 +73,8 @@ private extension UserNotificationManager { /// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil. /// - Warning: In certain scenarios, this will return the `faviconTemplateImage`. func thumbnailAttachment(for article: Article, webFeed: WebFeed) -> UNNotificationAttachment? { - if let image = article.iconImage() { - let fm = FileManager.default - var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0] - #if os(macOS) - path.appendPathComponent(webFeed.webFeedID + "_smallIcon.tiff") - #else - path.appendPathComponent(webFeed.webFeedID + "_smallIcon.png") - #endif - fm.createFile(atPath: path.path, contents: image.image.dataRepresentation()!, attributes: nil) - - let thumbnail = try? UNNotificationAttachment(identifier: webFeed.webFeedID, url: path, options: nil) + if let imageURL = article.iconImageUrl(webFeed: webFeed) { + let thumbnail = try? UNNotificationAttachment(identifier: webFeed.webFeedID, url: imageURL, options: nil) return thumbnail } return nil