From 640aaa9623a1353a47a3db556d6a5da04e40048c Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 11 Feb 2021 15:23:14 -0600 Subject: [PATCH 01/35] Update to the latest RSCore --- .../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 0ca6b48cc..71a837402 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -60,8 +60,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/RSCore.git", "state": { "branch": null, - "revision": "55295e826ac0249ac0c271e83c4489313b350a7c", - "version": "1.0.1" + "revision": "6b2ef2580968905af825c40442dc0ba3126032c0", + "version": "1.0.2" } }, { From b9b68bb48c27504e23da04b0edc135f1b1d160ce Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 13 Feb 2021 16:14:44 -0600 Subject: [PATCH 02/35] Correctly clear the progress bar for not found feeds and already subscribed feeds --- .../Sources/Account/CloudKit/CloudKitAccountDelegate.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index e5b9bc84d..daf96d4d2 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -673,14 +673,14 @@ private extension CloudKitAccountDelegate { case .success(let feedSpecifiers): guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), let url = URL(string: bestFeedSpecifier.urlString) else { BatchUpdate.shared.end() - self.refreshProgress.completeTasks(5) + self.refreshProgress.completeTasks(4) completion(.failure(AccountError.createErrorNotFound)) return } if account.hasWebFeed(withURL: bestFeedSpecifier.urlString) { BatchUpdate.shared.end() - self.refreshProgress.completeTasks(5) + self.refreshProgress.completeTasks(4) completion(.failure(AccountError.createErrorAlreadySubscribed)) return } From 828ca7ed2a96450b50ef3c976ddec0fff9e811c4 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 13 Feb 2021 16:25:17 -0600 Subject: [PATCH 03/35] Correctly clear the progress bar when a feed provider isn't able to find a feed --- Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index daf96d4d2..160c5e8c9 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -657,7 +657,7 @@ private extension CloudKitAccountDelegate { } case .failure: - self.refreshProgress.completeTasks(5) + self.refreshProgress.completeTasks(4) completion(.failure(AccountError.createErrorNotFound)) } } From b0a1183e115720306fc28d1abeffcfcb7b1d90fd Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 21 Feb 2021 18:00:40 -0800 Subject: [PATCH 04/35] Update URLs to use netnewswire.com where appropriate. --- Mac/AppDelegate.swift | 8 ++++---- Mac/CrashReporter/CrashReporter.swift | 2 +- Mac/Resources/Info.plist | 2 +- Multiplatform/iOS/Info.plist | 2 +- Multiplatform/iOS/SafariView.swift | 2 +- Multiplatform/iOS/Settings/About/About.rtf | 4 ++-- Multiplatform/iOS/Settings/SettingsModel.swift | 6 +++--- Multiplatform/macOS/Info.plist | 2 +- .../Advanced/AdvancedPreferencesView.swift | 2 +- README.md | 4 ++-- Shared/ArticleStyles/ArticleStyle.swift | 2 +- iOS/Resources/About.rtf | 6 +++--- iOS/Resources/Info.plist | 2 +- iOS/Settings/SettingsViewController.swift | 6 +++--- 14 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 23b5e819c..52b0abd0f 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -608,7 +608,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @IBAction func openWebsite(_ sender: Any?) { - Browser.open("https://ranchero.com/netnewswire/", inBackground: false) + Browser.open("https://netnewswire.com/", inBackground: false) } @IBAction func openReleaseNotes(_ sender: Any?) { @@ -632,7 +632,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } @IBAction func openSlackGroup(_ sender: Any?) { - Browser.open("https://ranchero.com/netnewswire/slack", inBackground: false) + Browser.open("https://netnewswire.com/slack", inBackground: false) } @IBAction func openTechnotes(_ sender: Any?) { @@ -642,7 +642,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @IBAction func showHelp(_ sender: Any?) { - Browser.open("https://ranchero.com/netnewswire/help/mac/5.1/en/", inBackground: false) + Browser.open("https://netnewswire.com/help/mac/5.1/en/", inBackground: false) } @IBAction func donateToAppCampForGirls(_ sender: Any?) { @@ -650,7 +650,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } @IBAction func showPrivacyPolicy(_ sender: Any?) { - Browser.open("https://ranchero.com/netnewswire/privacypolicy", inBackground: false) + Browser.open("https://netnewswire.com/privacypolicy", inBackground: false) } @IBAction func gotoToday(_ sender: Any?) { diff --git a/Mac/CrashReporter/CrashReporter.swift b/Mac/CrashReporter/CrashReporter.swift index 06aa3f5e6..9c15e602e 100644 --- a/Mac/CrashReporter/CrashReporter.swift +++ b/Mac/CrashReporter/CrashReporter.swift @@ -42,7 +42,7 @@ struct CrashReporter { } static func sendCrashLogText(_ crashLogText: String) { - var request = URLRequest(url: URL(string: "https://ranchero.com/netnewswire/crashreportcatcher.php")!) + var request = URLRequest(url: URL(string: "https://netnewswire.com/crashreportcatcher.php")!) request.httpMethod = HTTPMethod.post let boundary = "0xKhTmLbOuNdArY" diff --git a/Mac/Resources/Info.plist b/Mac/Resources/Info.plist index e2b17681d..f095490dd 100644 --- a/Mac/Resources/Info.plist +++ b/Mac/Resources/Info.plist @@ -72,6 +72,6 @@ SUFeedURL https://ranchero.com/downloads/netnewswire-release.xml UserAgent - NetNewsWire (RSS Reader; https://ranchero.com/netnewswire/) + NetNewsWire (RSS Reader; https://netnewswire.com/) diff --git a/Multiplatform/iOS/Info.plist b/Multiplatform/iOS/Info.plist index cab22bf05..5be8136b3 100644 --- a/Multiplatform/iOS/Info.plist +++ b/Multiplatform/iOS/Info.plist @@ -72,7 +72,7 @@ remote-notification UserAgent - NetNewsWire (RSS Reader; https://ranchero.com/netnewswire/) + NetNewsWire (RSS Reader; https://netnewswire.com/) OrganizationIdentifier $(ORGANIZATION_IDENTIFIER) DeveloperEntitlements diff --git a/Multiplatform/iOS/SafariView.swift b/Multiplatform/iOS/SafariView.swift index b3a96e718..62f1bc36c 100644 --- a/Multiplatform/iOS/SafariView.swift +++ b/Multiplatform/iOS/SafariView.swift @@ -58,6 +58,6 @@ struct SafariView: View { struct SafariView_Previews: PreviewProvider { static var previews: some View { - SafariView(url: URL(string: "https://ranchero.com/netnewswire/")!) + SafariView(url: URL(string: "https://netnewswire.com/")!) } } diff --git a/Multiplatform/iOS/Settings/About/About.rtf b/Multiplatform/iOS/Settings/About/About.rtf index 41b356e05..b8aa6f4e5 100644 --- a/Multiplatform/iOS/Settings/About/About.rtf +++ b/Multiplatform/iOS/Settings/About/About.rtf @@ -1,4 +1,4 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2511 +{\rtf1\ansi\ansicpg1252\cocoartf2513 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande-Bold;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red10\green96\blue255;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cssrgb\c0\c47843\c100000\cname systemBlueColor;} @@ -9,4 +9,4 @@ \fs22 \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 {\field{\*\fldinst{HYPERLINK "https://ranchero.com/netnewswire/"}}{\fldrslt -\fs28 \cf3 ranchero.com/netnewswire/}}} \ No newline at end of file +\fs28 \cf3 netnewswire.com}}} \ No newline at end of file diff --git a/Multiplatform/iOS/Settings/SettingsModel.swift b/Multiplatform/iOS/Settings/SettingsModel.swift index 63443d3fc..3c8218ae1 100644 --- a/Multiplatform/iOS/Settings/SettingsModel.swift +++ b/Multiplatform/iOS/Settings/SettingsModel.swift @@ -17,9 +17,9 @@ class SettingsModel: ObservableObject { var url: URL? { switch self { case .netNewsWireHelp: - return URL(string: "https://ranchero.com/netnewswire/help/ios/5.0/en/")! + return URL(string: "https://netnewswire.com/help/ios/5.0/en/")! case .netNewsWire: - return URL(string: "https://ranchero.com/netnewswire/")! + return URL(string: "https://netnewswire.com/")! case .supportNetNewsWire: return URL(string: "https://github.com/brentsimmons/NetNewsWire/blob/main/Technotes/HowToSupportNetNewsWire.markdown")! case .github: @@ -29,7 +29,7 @@ class SettingsModel: ObservableObject { case .technotes: return URL(string: "https://github.com/brentsimmons/NetNewsWire/tree/main/Technotes")! case .netNewsWireSlack: - return URL(string: "https://ranchero.com/netnewswire/slack")! + return URL(string: "https://netnewswire.com/slack")! case .releaseNotes: return URL.releaseNotes case .none: diff --git a/Multiplatform/macOS/Info.plist b/Multiplatform/macOS/Info.plist index c0b00660d..58377c4e4 100644 --- a/Multiplatform/macOS/Info.plist +++ b/Multiplatform/macOS/Info.plist @@ -30,7 +30,7 @@ UserAgent - NetNewsWire (RSS Reader; https://ranchero.com/netnewswire/) + NetNewsWire (RSS Reader; https://netnewswire.com/) OrganizationIdentifier $(ORGANIZATION_IDENTIFIER) DeveloperEntitlements diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Advanced/AdvancedPreferencesView.swift b/Multiplatform/macOS/Preferences/Preference Panes/Advanced/AdvancedPreferencesView.swift index 44e93770b..abcecfaed 100644 --- a/Multiplatform/macOS/Preferences/Preference Panes/Advanced/AdvancedPreferencesView.swift +++ b/Multiplatform/macOS/Preferences/Preference Panes/Advanced/AdvancedPreferencesView.swift @@ -30,7 +30,7 @@ struct AdvancedPreferencesView: View { HStack { Spacer() Button("Privacy Policy", action: { - NSWorkspace.shared.open(URL(string: "https://ranchero.com/netnewswire/privacypolicy")!) + NSWorkspace.shared.open(URL(string: "https://netnewswire.com/privacypolicy")!) }) Spacer() } diff --git a/README.md b/README.md index 2dbb595f9..e60220cbb 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ It’s a free and open source feed reader for macOS and iOS. It supports [RSS](http://cyber.harvard.edu/rss/rss.html), [Atom](https://tools.ietf.org/html/rfc4287), [JSON Feed](https://jsonfeed.org/), and [RSS-in-JSON](https://github.com/scripting/Scripting-News/blob/master/rss-in-json/README.md) formats. -More info: [https://ranchero.com/netnewswire/](https://ranchero.com/netnewswire/) +More info: [https://netnewswire.com/](https://netnewswire.com/) Also see the [Technotes](Technotes/) and the [Roadmap](Technotes/Roadmap.md). @@ -14,7 +14,7 @@ Here’s [How to Support NetNewsWire](Technotes/HowToSupportNetNewsWire.markdown #### Community -[Join the Slack group](https://ranchero.com/netnewswire/slack) to talk with other NetNewsWire users — and to help out, if you’d like to, by testing, coding, writing, providing feedback, or just helping us think things through. Everybody is welcome and encouraged to join. +[Join the Slack group](https://netnewswire.com/slack) to talk with other NetNewsWire users — and to help out, if you’d like to, by testing, coding, writing, providing feedback, or just helping us think things through. Everybody is welcome and encouraged to join. Every community member is expected to abide by the code of conduct which is included in the [Contributing](CONTRIBUTING.md) page. diff --git a/Shared/ArticleStyles/ArticleStyle.swift b/Shared/ArticleStyles/ArticleStyle.swift index 01f5d86fb..97b9a3030 100644 --- a/Shared/ArticleStyles/ArticleStyle.swift +++ b/Shared/ArticleStyles/ArticleStyle.swift @@ -24,7 +24,7 @@ struct ArticleStyle: Equatable { self.path = nil; self.emptyCSS = nil - self.info = ["CreatorHomePage": "https://ranchero.com/", "CreatorName": "Ranchero Software, LLC", "Version": "1.0"] + self.info = ["CreatorHomePage": "https://netnewswire.com/", "CreatorName": "Ranchero Software", "Version": "1.0"] let sharedCSSPath = Bundle.main.path(forResource: "shared", ofType: "css")! let platformCSSPath = Bundle.main.path(forResource: "styleSheet", ofType: "css")! diff --git a/iOS/Resources/About.rtf b/iOS/Resources/About.rtf index 41b356e05..8bf10c2d4 100644 --- a/iOS/Resources/About.rtf +++ b/iOS/Resources/About.rtf @@ -1,4 +1,4 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2511 +{\rtf1\ansi\ansicpg1252\cocoartf2513 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande-Bold;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red10\green96\blue255;} {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cssrgb\c0\c47843\c100000\cname systemBlueColor;} @@ -8,5 +8,5 @@ \f0\b\fs28 \cf2 By Brent Simmons and the Ranchero Software team \fs22 \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 -{\field{\*\fldinst{HYPERLINK "https://ranchero.com/netnewswire/"}}{\fldrslt -\fs28 \cf3 ranchero.com/netnewswire/}}} \ No newline at end of file +{\field{\*\fldinst{HYPERLINK "https://netnewswire.com/"}}{\fldrslt +\fs28 \cf3 netnewswire.com}}} \ No newline at end of file diff --git a/iOS/Resources/Info.plist b/iOS/Resources/Info.plist index e843a3e9e..24e877697 100644 --- a/iOS/Resources/Info.plist +++ b/iOS/Resources/Info.plist @@ -176,6 +176,6 @@ UserAgent - NetNewsWire (RSS Reader; https://ranchero.com/netnewswire/) + NetNewsWire (RSS Reader; https://netnewswire.com/) diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index ae4497bb0..62ad0dda3 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -226,10 +226,10 @@ class SettingsViewController: UITableViewController { case 7: switch indexPath.row { case 0: - openURL("https://ranchero.com/netnewswire/help/ios/5.0/en/") + openURL("https://netnewswire.com/help/ios/5.0/en/") tableView.selectRow(at: nil, animated: true, scrollPosition: .none) case 1: - openURL("https://ranchero.com/netnewswire/") + openURL("https://netnewswire.com/") tableView.selectRow(at: nil, animated: true, scrollPosition: .none) case 2: openURL(URL.releaseNotes.absoluteString) @@ -247,7 +247,7 @@ class SettingsViewController: UITableViewController { openURL("https://github.com/brentsimmons/NetNewsWire/tree/main/Technotes") tableView.selectRow(at: nil, animated: true, scrollPosition: .none) case 7: - openURL("https://ranchero.com/netnewswire/slack") + openURL("https://netnewswire.com/slack") tableView.selectRow(at: nil, animated: true, scrollPosition: .none) case 8: let timeline = UIStoryboard.settings.instantiateController(ofType: AboutViewController.self) From f00241e73e00d17bfccd1021a22d9c075c5bf3c3 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 24 Feb 2021 07:47:52 +0800 Subject: [PATCH 05/35] revised context menu code --- iOS/MasterFeed/MasterFeedViewController.swift | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index acf0a10bb..c2f719e6e 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -589,40 +589,50 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { @objc func configureContextMenu(_: Any? = nil) { if #available(iOS 14.0, *) { + + /* + Context Menu Order: + 1. Add Web Feed + 2. Add Reddit Feed + 3. Add Twitter Feed + 4. Add Folder + */ + + var menuItems: [UIAction] = [] + let addWebFeedActionTitle = NSLocalizedString("Add Web Feed", comment: "Add Web Feed") - let addWebFeedAction = UIAction(title: addWebFeedActionTitle, image: AppAssets.faviconTemplateImage.withRenderingMode(.alwaysOriginal).withTintColor(.secondaryLabel)) { _ in + let addWebFeedAction = UIAction(title: addWebFeedActionTitle, image: AppAssets.plus) { _ in self.coordinator.showAddWebFeed() } - - let addRedditFeedActionTitle = NSLocalizedString("Add Reddit Feed", comment: "Add Reddit Feed") - let addRedditFeedAction = UIAction(title: addRedditFeedActionTitle, image: AppAssets.redditOriginal) { _ in - self.coordinator.showAddRedditFeed() - } - - let addTwitterFeedActionTitle = NSLocalizedString("Add Twitter Feed", comment: "Add Twitter Feed") - let addTwitterFeedAction = UIAction(title: addTwitterFeedActionTitle, image: AppAssets.twitterOriginal) { _ in - self.coordinator.showAddTwitterFeed() - } - - let addWebFolderdActionTitle = NSLocalizedString("Add Folder", comment: "Add Folder") - let addWebFolderAction = UIAction(title: addWebFolderdActionTitle, image: AppAssets.masterFolderImageNonIcon) { _ in - self.coordinator.showAddFolder() - } - - var children = [addWebFolderAction, addWebFeedAction] - + menuItems.append(addWebFeedAction) if AccountManager.shared.activeAccounts.contains(where: { $0.type == .onMyMac || $0.type == .cloudKit }) { if ExtensionPointManager.shared.isRedditEnabled { - children.insert(addRedditFeedAction, at: 0) + let addRedditFeedActionTitle = NSLocalizedString("Add Reddit Feed", comment: "Add Reddit Feed") + let addRedditFeedAction = UIAction(title: addRedditFeedActionTitle, image: AppAssets.contextMenuReddit.tinted(color: .label)) { _ in + self.coordinator.showAddRedditFeed() + } + menuItems.append(addRedditFeedAction) } if ExtensionPointManager.shared.isTwitterEnabled { - children.insert(addTwitterFeedAction, at: 0) + let addTwitterFeedActionTitle = NSLocalizedString("Add Twitter Feed", comment: "Add Twitter Feed") + let addTwitterFeedAction = UIAction(title: addTwitterFeedActionTitle, image: AppAssets.contextMenuTwitter.tinted(color: .label)) { _ in + self.coordinator.showAddTwitterFeed() + } + menuItems.append(addTwitterFeedAction) } } - let menu = UIMenu(title: "Add Item", image: nil, identifier: nil, options: [], children: children) + + let addWebFolderActionTitle = NSLocalizedString("Add Folder", comment: "Add Folder") + let addWebFolderAction = UIAction(title: addWebFolderActionTitle, image: AppAssets.folderOutlinePlus) { _ in + self.coordinator.showAddFolder() + } - self.addNewItemButton.menu = menu + menuItems.append(addWebFolderAction) + + let contextMenu = UIMenu(title: NSLocalizedString("Add Item", comment: "Add Item"), image: nil, identifier: nil, options: [], children: menuItems.reversed()) + + self.addNewItemButton.menu = contextMenu } } From 4e882e7285f2022497f02db0f8206ff87e7a9f4b Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 24 Feb 2021 07:49:47 +0800 Subject: [PATCH 06/35] adds assets --- .../contextMenuReddit.imageset/Contents.json | 12 ++++++++++++ .../redditContextMenu.pdf | Bin 0 -> 5484 bytes .../contextMenuTwitter.imageset/Contents.json | 12 ++++++++++++ .../twitterContextMenu.pdf | Bin 0 -> 4835 bytes 4 files changed, 24 insertions(+) create mode 100644 iOS/Resources/Assets.xcassets/contextMenuReddit.imageset/Contents.json create mode 100644 iOS/Resources/Assets.xcassets/contextMenuReddit.imageset/redditContextMenu.pdf create mode 100644 iOS/Resources/Assets.xcassets/contextMenuTwitter.imageset/Contents.json create mode 100644 iOS/Resources/Assets.xcassets/contextMenuTwitter.imageset/twitterContextMenu.pdf diff --git a/iOS/Resources/Assets.xcassets/contextMenuReddit.imageset/Contents.json b/iOS/Resources/Assets.xcassets/contextMenuReddit.imageset/Contents.json new file mode 100644 index 000000000..21dcfdf56 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/contextMenuReddit.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "redditContextMenu.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/Resources/Assets.xcassets/contextMenuReddit.imageset/redditContextMenu.pdf b/iOS/Resources/Assets.xcassets/contextMenuReddit.imageset/redditContextMenu.pdf new file mode 100644 index 0000000000000000000000000000000000000000..aac84d6201cc7b79077bd4b0e8dee2307ca90778 GIT binary patch literal 5484 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 UF54e-Dhw7BBLxB#G!#ky2NPJv#{d8T literal 0 HcmV?d00001 diff --git a/iOS/Resources/Assets.xcassets/contextMenuTwitter.imageset/Contents.json b/iOS/Resources/Assets.xcassets/contextMenuTwitter.imageset/Contents.json new file mode 100644 index 000000000..1cf455ca4 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/contextMenuTwitter.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "twitterContextMenu.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/Resources/Assets.xcassets/contextMenuTwitter.imageset/twitterContextMenu.pdf b/iOS/Resources/Assets.xcassets/contextMenuTwitter.imageset/twitterContextMenu.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c7d5a5d74873444c0beeaeb63c2f7530a30204c4 GIT binary patch literal 4835 zcmai&2UJtb+Js`+`oEi#Bx4?o>0B7sm*4#?o_RG($?Eokc z1Vo$L0wg7YLP|FFSd=s2*B*gIDWELSNEA>=4drNswFZL0AaS6yGysEjMj;#k?!>wY zHIAHO)WJs{fFWW%ep3$3w-EzWmpq6_tBBNngCe|6o(#Nr4WDYph_i+t9&Go!9dJ!5 z4qKO7g$IjHvTW^zMb#zcZzPUpkd8=7p!ZkHk?&sQ4v$*d?=ctH%&vZVT)BTiTHo$z z{KESMv*J;$1UDAkt+%1w4_3wxnejt6)i__bvB`f#>KkDS<68aWB^dyF4(3HPg(*zs zEb4rK%WP{3?a^N)Ec+3UO3ix{3#`i$C@9T8@^+7k&eT+;6wxd^xjCvg!&x53C$5#B zE5MxDe?fR)YW23fWNx@vrhvYMw6t5-Yst1Lx+;4&nZwP~xef=cL9 zN=FhD+MABzWj1UcXLN=m-i|w73t;A_v}W$gjv5Qj4t@`=F{vFfAy1w!!I4d7HucFC z>gV^x1YbEqu+nL5HQ+7N+Ll+*gRr%UCk{1?O9{scdeB`f+@MCZ#Y)Z`Y7qve5&R}$ zPY-iibhLZX=cEZ|W>_jZdWET|>zKt^@66hVP+iCyD|psp2jRdjzi8!-8v1BSm5|V; zzd$V)9c)w(o#Uvhv)k_9$6@q^YYtMaZ5YinUU<*8EBY#bJV&Ll)skXX3Jw1)TUK&D z_1W3DuTpkekB&8RlgjPXgMZRkQbbl&lAm=!uW+>*_ro8~z7s_hy! zqdb;D^Q!>;k?p+sdk# z7Ga7n*PI;N)0QP9_IPLarcL9=WItyc(#FqnV{5QGj zGEDH`V?bp3&AMTkCrUticZpFZN+S3TL=G4UqFgtwD7;?SoRKP_VBdN#Jh1ZBgMq=a z4qqZ_qA$~&-7R8D+7$xp8mtSK$uT@mm82x&q7S6_BOr9lS!6dv>g7ao@DmP|eC5hNB{02ZdBL9*X!qb^3fqxSx9S8Q?0-1o%w%>orW{oLK}qz8WD;rC;?4?Lh@*P zw6nGo!U6?6Q$u+S7%1{r0ickgJ62g6OVGuiK4pSW^vnm4mj12&ndw*kKj-^*nvey8 z;Qf0pV!y?z6J{zTdnUe)XCnwHpj>S%P+H1z|CV2fC>Zj;1^RbQPD^yt(H>zA!Pkk^ zeZIgB`&H=)hG59&eUgIUuiPFaIw2} zkK{5vF|9svla-g9|fs>J!T@CeLx62%tD9 zD(TI2X^tdb8`s7HIVJnBlfVlJ21A%LDYIU)&$O1>@8-?(*K|Ek;+oB5HRxff6;6+= z(U?f#oX(CU%QSw@Ic1gogdA0;qmn{2x5v}pzyCsw2*j9d_HltL(>@HM$jGuDwnaij z5#I`{b*7CtfLW&#x#wEVhLeSX?ak zISf;;;_4H0i7Fh<%gjx`hHn6gw$2e%OuX2>7Hn_&to0i;H&w*ysy&7{!%mQYQS2zg zAN`>r7`B((y?&%8k$dTm-%S-JC`xfIH!oLJ;p0bY+9W*kWRL!IrV(cIsjEfcMMmSL z&7ehH&Bb`iz%c8YAW1*{r-Dd8pwHm6&t*|=au)A2J4S04Ga|1bm`0@vc_&(mNd6`% zhvOYj60a2_dN81~zSg=pwiFWs@%&@q@H&-oxZ|{%EW7snE_DpjnKMg(C3`Z^zKP_* zQ*!J}fUut+h(zEa4Vy32iF5);6zF@Go-~nE@Ggms4A~o5=`Tb8S@0KP*8p25q5(h8 z&t&TUqE6?H$sRP)I8jmiD$4+Zn^Zx>6Mi-n{@>}DvSbUNGUw1G%MHtw zByE!qdrJRI=HN3|{$;*^!c2J$shyix)@llqfWw#MpMk>XPl&w&8J?5$BWS)5z4IL@ zlVGDzZtk8^aA$f!eyMSE`d%foH0}H5OVd}r6A1*FHOEoNbW)J^D(O>P*;J5ws?R~p zaKkQ^J%^+~G36<3&XuaDyRnaENzaF5BA#DnZfBVZF^HTt*Fy}M-vbRkw|K6nc4=PJ zk{;@J;Qz{eusm0v`}G}HN>PT?s9UXi4`Uu0l_iyvenG5Cu+erjvV3E6@~*sW3~>}G zA$w0Z99iSL(n2%Mwdr2VH^I2!PuGO`ZvNy(J-~;}=h|e<_4Wqtr<8_dcK&oX5`i4{ z96I!@6blq5feg*Untu6kEPF7$N2{?pZ+Y|=*;*AgF7-G@m0*rbS{%H|6@EG@w(KTc zQe1U6;<|1lm7Ome$e6-QI5L1{oOI9bXvFhyF~aY$Dawtg!|qkdc*uCjztW<~cO1|! zd|sVq2(MG{y5}XN90yI>PMYZ|jw_0_OIS&I%n6SlPI5~!6U^W?yWw;1kKE%*63r%= zCT+j$GQCWt>568lAQi=wtOWB=^Az(i^MG$6ZJM05&-|XfzW^_gSt{$}e076WuK$%s zUuDJpirZgNUqM%u#d41NdUh0nvgrQ_4ggimLWLzc^`!#pbwZGW^ZOD)H-rm)ttvass1CJDETu%tMqbfwIt zEC_d0SQ}EkB{Q5eTzG2=N=RP+sAI{Ai-2Vom#pR8>XptJFOn*@$j9khWvkIx5WjwCS4oWyd+Xg%Ep;%)NAm&w7cv_Tt{GXsSx*&I@-N2SXZ8! zK(9fDmBs1Gp3TS^a_-06<^3volW7J$ygf8M*;Le*zF!)+b=kT1(R3x7bF3gzFw)H3 zDRV$~G-1YYiY zyl~Eitfh&#smA}hP@frx@c=W4+_AiCkR+uDwF%Q9=CYuG@N2GBk4jy4Z+ADcTjq)i z@~t&@V>esz93SI~_wd1}^V@QCl)mFrh0=}*HhHW$pPmF$>Rxy_Lc z$JK-!JzHq!Ux?Z?*+(76k|hPc3G7u!YBdS7P$^MiQkfxv%f6j*d1JQaH(L<@G(MXb zTF_hY$tJL3zoK7`6A@mf3O)`*S(k}(;KVCgE6qIc^>_T+K;=D z9I2cE22?oPHxpLMsEiMq9~tI%W>aPzyRUX_cO7|)ScB#4jDU;&4k6T$=?`s~c*6SgR9zCS9i9yCxK=R+z-7B?K9?VlXY<)^c@&m5-T^!rI-< zSiEwO@~e*K7?(kbfnl+Hv3SwusAuivGmkpZCsRMWbOEw34!tiEh87Qq#>U=Yr9?qhsBqBC8UZ ziG)|0SKV6hNz~A_n)F1oRd1W6w;K+1OHR0x#F^sg`g2|jhbBit6T8)3bJ3@1S4bny zAFd15KRz`blUnkcIO!!XpfkPm(Np}y`ov;&CW{*Y=SwfVQ+zVGKWtf0x3l_k_#qw_ zY84Y)DKB)Ia8L?wIfpO6^D9*-Eh`r*<0B6u$34q;+P-&3CiTw*HSqrNZZDy?D8t+B zsLsl~tM$8G?@IEWdlUKfu*0EUhXv%~Y`>e$Ms8mCL{a^nUB8peBa5Am*riqYK0F~K z>Wzoiqv9_eC1ILF{|Kv=-Ffxl9~U_po=7--BR=VD`z z1VTUxV#3C}Kus5fGu9JG=mCDV4enT80{RoqIIx6a6bLj9;8C~@0YShZ2m}g-iau=-(GmR73 Date: Tue, 23 Feb 2021 17:59:19 -0600 Subject: [PATCH 07/35] Modify CloudKit add logic so that we don't wait to sync the statuses before completing the process. Fixes #2827 --- .../CloudKit/CloudKitAccountDelegate.swift | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index 160c5e8c9..15aeebfeb 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -636,7 +636,9 @@ private extension CloudKitAccountDelegate { account.update(urlString, with: parsedItems) { result in switch result { case .success: - self.sendNewArticlesToTheCloud(account, feed, completion: completion) + self.sendNewArticlesToTheCloud(account, feed) + self.refreshProgress.clear() + completion(.success(feed)) case .failure(let error): self.refreshProgress.completeTasks(2) completion(.failure(error)) @@ -708,7 +710,9 @@ private extension CloudKitAccountDelegate { switch result { case .success(let externalID): feed.externalID = externalID - self.sendNewArticlesToTheCloud(account, feed, completion: completion) + self.sendNewArticlesToTheCloud(account, feed) + self.refreshProgress.clear() + completion(.success(feed)) case .failure(let error): container.removeWebFeed(feed) self.refreshProgress.completeTasks(2) @@ -740,7 +744,7 @@ private extension CloudKitAccountDelegate { } } - func sendNewArticlesToTheCloud(_ account: Account, _ feed: WebFeed, completion: @escaping (Result) -> Void) { + func sendNewArticlesToTheCloud(_ account: Account, _ feed: WebFeed) { account.fetchArticlesAsync(.webFeed(feed)) { result in switch result { case .success(let articles): @@ -749,19 +753,14 @@ private extension CloudKitAccountDelegate { self.sendArticleStatus(for: account, showProgress: true) { result in switch result { case .success: - self.articlesZone.fetchChangesInZone() { _ in - self.refreshProgress.completeTask() - completion(.success(feed)) - } + self.articlesZone.fetchChangesInZone() { _ in } case .failure(let error): - self.refreshProgress.clear() - completion(.failure(error)) + os_log(.error, log: self.log, "CloudKit Feed send articles error: %@.", error.localizedDescription) } } } case .failure(let error): - self.refreshProgress.clear() - completion(.failure(error)) + os_log(.error, log: self.log, "CloudKit Feed send articles error: %@.", error.localizedDescription) } } } From 38799d48484ffbf8e05009dc6307d843c3c6330c Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 24 Feb 2021 08:07:09 +0800 Subject: [PATCH 08/35] Adds missing vars to AppAssets.swift --- iOS/AppAssets.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index d2fa7bad8..2a5ed0659 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -101,6 +101,14 @@ struct AppAssets { return UIImage(named: "disclosure")! }() + static var contextMenuReddit: UIImage = { + return UIImage(named: "contextMenuReddit")! + }() + + static var contextMenuTwitter: UIImage = { + return UIImage(named: "contextMenuTwitter")! + }() + static var copyImage: UIImage = { return UIImage(systemName: "doc.on.doc")! }() @@ -133,6 +141,10 @@ struct AppAssets { UIImage(systemName: "line.horizontal.3.decrease.circle.fill")! }() + static var folderOutlinePlus: UIImage = { + UIImage(systemName: "folder.badge.plus")! + }() + static var fullScreenBackgroundColor: UIColor = { return UIColor(named: "fullScreenBackgroundColor")! }() @@ -173,6 +185,10 @@ struct AppAssets { return UIImage(systemName: "chevron.down.circle")! }() + static var plus: UIImage = { + UIImage(systemName: "plus")! + }() + static var prevArticleImage: UIImage = { return UIImage(systemName: "chevron.up")! }() From 42930371b8d27d827612f459cb22606ef18f16f9 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 24 Feb 2021 15:14:38 -0600 Subject: [PATCH 09/35] Compress html and text content --- .../CloudKit/CloudKitArticlesZone.swift | 73 +++++++++++++++---- .../CloudKitArticlesZoneDelegate.swift | 21 +++++- 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift index 095ddcdea..6947e9c6d 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift @@ -25,6 +25,8 @@ final class CloudKitArticlesZone: CloudKitZone { weak var database: CKDatabase? var delegate: CloudKitZoneDelegate? = nil + var compressionQueue = DispatchQueue(label: "Articles Zone Compression Queue") + struct CloudKitArticle { static let recordType = "Article" struct Fields { @@ -33,7 +35,9 @@ final class CloudKitArticlesZone: CloudKitZone { static let uniqueID = "uniqueID" static let title = "title" static let contentHTML = "contentHTML" + static let contentHTMLData = "contentHTMLData" static let contentText = "contentText" + static let contentTextData = "contentTextData" static let url = "url" static let externalURL = "externalURL" static let summary = "summary" @@ -96,7 +100,11 @@ final class CloudKitArticlesZone: CloudKitZone { records.append(makeArticleRecord(saveArticle)) } - save(records, completion: completion) + compressionQueue.async { + self.compressArticleRecords(records) { compressedRecords in + self.save(compressedRecords, completion: completion) + } + } } func deleteArticles(_ webFeedExternalID: String, completion: @escaping ((Result) -> Void)) { @@ -130,22 +138,29 @@ final class CloudKitArticlesZone: CloudKitZone { deleteRecordIDs.append(CKRecord.ID(recordName: self.articleID(statusUpdate.articleID), zoneID: zoneID)) } } - - self.modify(recordsToSave: modifyRecords, recordIDsToDelete: deleteRecordIDs) { result in - switch result { - case .success: - self.saveIfNew(newRecords) { result in - switch result { - case .success: - completion(.success(())) - case .failure(let error): - completion(.failure(error)) + + compressionQueue.async { + self.compressArticleRecords(modifyRecords) { compressedModifyRecords in + self.compressArticleRecords(newRecords) { compressedNewRecords in + self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDs) { result in + switch result { + case .success: + self.saveIfNew(compressedNewRecords) { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } + case .failure(let error): + self.handleModifyArticlesError(error, statusUpdates: statusUpdates, completion: completion) + } } } - case .failure(let error): - self.handleModifyArticlesError(error, statusUpdates: statusUpdates, completion: completion) } } + } } @@ -237,5 +252,37 @@ private extension CloudKitArticlesZone { return record } + func compressArticleRecords(_ records: [CKRecord], completion: ([CKRecord]) -> Void ) { + var result = [CKRecord]() + + for record in records { + + if record.recordType == CloudKitArticle.recordType { + + if let contentHTML = record[CloudKitArticle.Fields.contentHTML] as? String { + let data = Data(contentHTML.utf8) as NSData + if let compressedData = try? data.compressed(using: .lzfse) { + record[CloudKitArticle.Fields.contentHTMLData] = compressedData + record[CloudKitArticle.Fields.contentHTML] = nil + } + } + + if let contentText = record[CloudKitArticle.Fields.contentText] as? String { + let data = Data(contentText.utf8) as NSData + if let compressedData = try? data.compressed(using: .lzfse) { + record[CloudKitArticle.Fields.contentTextData] = compressedData + record[CloudKitArticle.Fields.contentText] = nil + } + } + + } else { + + result.append(record) + + } + } + + completion(result) + } } diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift index c3801c1ec..2f20b4204 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZoneDelegate.swift @@ -23,6 +23,7 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate { weak var account: Account? var database: SyncDatabase weak var articlesZone: CloudKitArticlesZone? + var compressionQueue = DispatchQueue(label: "Articles Zone Delegate Compression Queue") init(account: Account, database: SyncDatabase, articlesZone: CloudKitArticlesZone) { self.account = account @@ -134,7 +135,7 @@ private extension CloudKitArticlesZoneDelegate { } group.enter() - DispatchQueue.global(qos: .utility).async { + compressionQueue.async { let parsedItems = records.compactMap { self.makeParsedItem($0) } let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) } @@ -199,6 +200,20 @@ private extension CloudKitArticlesZoneDelegate { return nil } + var contentHTML = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentHTML] as? String + if let contentHTMLData = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentHTMLData] as? NSData { + if let decompressedContentHTMLData = try? contentHTMLData.decompressed(using: .lzfse) { + contentHTML = String(data: decompressedContentHTMLData as Data, encoding: .utf8) + } + } + + var contentText = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentText] as? String + if let contentTextData = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentTextData] as? NSData { + if let decompressedContentTextData = try? contentTextData.decompressed(using: .lzfse) { + contentText = String(data: decompressedContentTextData as Data, encoding: .utf8) + } + } + let parsedItem = ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: webFeedURL, @@ -206,8 +221,8 @@ private extension CloudKitArticlesZoneDelegate { externalURL: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.externalURL] as? String, title: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.title] as? String, language: nil, - contentHTML: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentHTML] as? String, - contentText: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentText] as? String, + contentHTML: contentHTML, + contentText: contentText, summary: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.summary] as? String, imageURL: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.imageURL] as? String, bannerImageURL: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.imageURL] as? String, From bb99e6f69c76868565cb0bb9413a9da2d27f43b7 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 24 Feb 2021 15:33:24 -0600 Subject: [PATCH 10/35] Simplify the compression logic --- .../CloudKit/CloudKitArticlesZone.swift | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift index 6947e9c6d..5c8733317 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift @@ -101,9 +101,8 @@ final class CloudKitArticlesZone: CloudKitZone { } compressionQueue.async { - self.compressArticleRecords(records) { compressedRecords in - self.save(compressedRecords, completion: completion) - } + let compressedRecords = self.compressArticleRecords(records) + self.save(compressedRecords, completion: completion) } } @@ -140,23 +139,21 @@ final class CloudKitArticlesZone: CloudKitZone { } compressionQueue.async { - self.compressArticleRecords(modifyRecords) { compressedModifyRecords in - self.compressArticleRecords(newRecords) { compressedNewRecords in - self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDs) { result in + let compressedModifyRecords = self.compressArticleRecords(modifyRecords) + self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDs) { result in + switch result { + case .success: + let compressedNewRecords = self.compressArticleRecords(newRecords) + self.saveIfNew(compressedNewRecords) { result in switch result { case .success: - self.saveIfNew(compressedNewRecords) { result in - switch result { - case .success: - completion(.success(())) - case .failure(let error): - completion(.failure(error)) - } - } + completion(.success(())) case .failure(let error): - self.handleModifyArticlesError(error, statusUpdates: statusUpdates, completion: completion) + completion(.failure(error)) } } + case .failure(let error): + self.handleModifyArticlesError(error, statusUpdates: statusUpdates, completion: completion) } } } @@ -252,7 +249,7 @@ private extension CloudKitArticlesZone { return record } - func compressArticleRecords(_ records: [CKRecord], completion: ([CKRecord]) -> Void ) { + func compressArticleRecords(_ records: [CKRecord]) -> [CKRecord] { var result = [CKRecord]() for record in records { @@ -262,7 +259,7 @@ private extension CloudKitArticlesZone { if let contentHTML = record[CloudKitArticle.Fields.contentHTML] as? String { let data = Data(contentHTML.utf8) as NSData if let compressedData = try? data.compressed(using: .lzfse) { - record[CloudKitArticle.Fields.contentHTMLData] = compressedData + record[CloudKitArticle.Fields.contentHTMLData] = compressedData as Data record[CloudKitArticle.Fields.contentHTML] = nil } } @@ -270,19 +267,17 @@ private extension CloudKitArticlesZone { if let contentText = record[CloudKitArticle.Fields.contentText] as? String { let data = Data(contentText.utf8) as NSData if let compressedData = try? data.compressed(using: .lzfse) { - record[CloudKitArticle.Fields.contentTextData] = compressedData + record[CloudKitArticle.Fields.contentTextData] = compressedData as Data record[CloudKitArticle.Fields.contentText] = nil } } - } else { - - result.append(record) - } + + result.append(record) } - completion(result) + return result } } From 53e0354f939754e71b662a7e4ee4683bd38c3c9d Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 24 Feb 2021 16:00:01 -0600 Subject: [PATCH 11/35] Removed datePublished force unwrap --- Shared/Widget/WidgetDataEncoder.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 2a38cdf48..5b928cea3 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -47,7 +47,7 @@ public final class WidgetDataEncoder { articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleSummary: article.summary, feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished!.description) + pubDate: article.datePublished?.description ?? "") unread.append(latestArticle) if unread.count == 7 { break } } @@ -58,7 +58,7 @@ public final class WidgetDataEncoder { articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleSummary: article.summary, feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished!.description) + pubDate: article.datePublished?.description ?? "") starred.append(latestArticle) if starred.count == 7 { break } } @@ -69,7 +69,7 @@ public final class WidgetDataEncoder { articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleSummary: article.summary, feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished!.description) + pubDate: article.datePublished?.description ?? "") today.append(latestArticle) if today.count == 7 { break } } From 59ceac43dd323a7379cdf45de1a023ed099bacd1 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 25 Feb 2021 20:51:04 +0800 Subject: [PATCH 12/35] changes background colour on extension views --- iOS/Add/Reddit/RedditAdd.storyboard | 22 +++++++++++----------- iOS/Add/Twitter/TwitterAdd.storyboard | 14 +++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/iOS/Add/Reddit/RedditAdd.storyboard b/iOS/Add/Reddit/RedditAdd.storyboard index 1add6b8b2..738c51e87 100644 --- a/iOS/Add/Reddit/RedditAdd.storyboard +++ b/iOS/Add/Reddit/RedditAdd.storyboard @@ -1,9 +1,9 @@ - + - + @@ -15,7 +15,7 @@ - + @@ -58,7 +58,7 @@ diff --git a/iOS/Add/Twitter/TwitterAdd.storyboard b/iOS/Add/Twitter/TwitterAdd.storyboard index 87c3d69cd..f6b2db210 100644 --- a/iOS/Add/Twitter/TwitterAdd.storyboard +++ b/iOS/Add/Twitter/TwitterAdd.storyboard @@ -1,9 +1,9 @@ - + - + @@ -31,7 +31,7 @@ - + @@ -159,7 +159,7 @@ - + @@ -197,7 +197,7 @@ - + @@ -240,8 +240,8 @@ - - + + From 13dd1d1bb5ec57b17d0a241cd8e12b752dea5c96 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Sat, 6 Mar 2021 10:45:58 +0800 Subject: [PATCH 13/35] tweaks to pre/code css styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows Apple’s example. • `code` within normal text is sized to 1em • `code` within `pre` is sized slightly smaller and the letter-spacing is tightened --- Shared/Article Rendering/shared.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Shared/Article Rendering/shared.css b/Shared/Article Rendering/shared.css index fa2a5920d..9989734c4 100644 --- a/Shared/Article Rendering/shared.css +++ b/Shared/Article Rendering/shared.css @@ -137,7 +137,6 @@ pre { margin: 0; overflow: auto; overflow-y: hidden; - word-wrap: normal; word-break: normal; } @@ -145,12 +144,18 @@ pre { pre { line-height: 1.4286em; } + code, pre { font-family: "SF Mono", Menlo, "Courier New", Courier, monospace; - font-size: .8235em; + font-size: 1em; -webkit-hyphens: none; } +pre code { + letter-spacing: -.027em; + font-size: 0.9375em; +} + .nnw-overflow { overflow-x: auto; } From d0e3ec6d1c21570a748a703874620adec8fb4e89 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 6 Mar 2021 16:25:10 -0600 Subject: [PATCH 14/35] Fix variable name --- Mac/MainWindow/IconView.swift | 12 ++++++------ Multiplatform/iOS/Article/IconView.swift | 12 ++++++------ Multiplatform/macOS/Article/IconView.swift | 12 ++++++------ iOS/IconView.swift | 12 ++++++------ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Mac/MainWindow/IconView.swift b/Mac/MainWindow/IconView.swift index 401f41cb0..edbbbace8 100644 --- a/Mac/MainWindow/IconView.swift +++ b/Mac/MainWindow/IconView.swift @@ -17,15 +17,15 @@ final class IconView: NSView { if NSApplication.shared.effectiveAppearance.isDarkMode { if self.iconImage?.isDark ?? false { - self.isDisconcernable = false + self.isDiscernable = false } else { - self.isDisconcernable = true + self.isDiscernable = true } } else { if self.iconImage?.isBright ?? false { - self.isDisconcernable = false + self.isDiscernable = false } else { - self.isDisconcernable = true + self.isDiscernable = true } } @@ -35,7 +35,7 @@ final class IconView: NSView { } } - private var isDisconcernable = true + private var isDiscernable = true override var isFlipped: Bool { return true @@ -85,7 +85,7 @@ final class IconView: NSView { override func draw(_ dirtyRect: NSRect) { guard !(iconImage?.isBackgroundSupressed ?? false) else { return } - guard hasExposedVerticalBackground || !isDisconcernable else { return } + guard hasExposedVerticalBackground || !isDiscernable else { return } let color = NSApplication.shared.effectiveAppearance.isDarkMode ? IconView.darkBackgroundColor : IconView.lightBackgroundColor color.set() diff --git a/Multiplatform/iOS/Article/IconView.swift b/Multiplatform/iOS/Article/IconView.swift index b187821cf..dc44dc0d6 100644 --- a/Multiplatform/iOS/Article/IconView.swift +++ b/Multiplatform/iOS/Article/IconView.swift @@ -17,18 +17,18 @@ final class IconView: UIView { if self.traitCollection.userInterfaceStyle == .dark { if self.iconImage?.isDark ?? false { - self.isDisconcernable = false + self.isDiscernable = false self.setNeedsLayout() } else { - self.isDisconcernable = true + self.isDiscernable = true self.setNeedsLayout() } } else { if self.iconImage?.isBright ?? false { - self.isDisconcernable = false + self.isDiscernable = false self.setNeedsLayout() } else { - self.isDisconcernable = true + self.isDiscernable = true self.setNeedsLayout() } } @@ -37,7 +37,7 @@ final class IconView: UIView { } } - private var isDisconcernable = true + private var isDiscernable = true private let imageView: UIImageView = { let imageView = UIImageView(image: AppAssets.faviconTemplateImage) @@ -79,7 +79,7 @@ final class IconView: UIView { override func layoutSubviews() { imageView.setFrameIfNotEqual(rectForImageView()) - if (iconImage != nil && isVerticalBackgroundExposed) || !isDisconcernable { + if (iconImage != nil && isVerticalBackgroundExposed) || !isDiscernable { backgroundColor = AppAssets.uiIconBackgroundColor } else { backgroundColor = nil diff --git a/Multiplatform/macOS/Article/IconView.swift b/Multiplatform/macOS/Article/IconView.swift index 7a721c4f8..6513d485d 100644 --- a/Multiplatform/macOS/Article/IconView.swift +++ b/Multiplatform/macOS/Article/IconView.swift @@ -17,15 +17,15 @@ final class IconView: NSView { if NSApplication.shared.effectiveAppearance.isDarkMode { if self.iconImage?.isDark ?? false { - self.isDisconcernable = false + self.isDiscernable = false } else { - self.isDisconcernable = true + self.isDiscernable = true } } else { if self.iconImage?.isBright ?? false { - self.isDisconcernable = false + self.isDiscernable = false } else { - self.isDisconcernable = true + self.isDiscernable = true } } @@ -35,7 +35,7 @@ final class IconView: NSView { } } - private var isDisconcernable = true + private var isDiscernable = true override var isFlipped: Bool { return true @@ -81,7 +81,7 @@ final class IconView: NSView { } override func draw(_ dirtyRect: NSRect) { - guard hasExposedVerticalBackground || !isDisconcernable else { + guard hasExposedVerticalBackground || !isDiscernable else { return } diff --git a/iOS/IconView.swift b/iOS/IconView.swift index e342c010f..55cd454cc 100644 --- a/iOS/IconView.swift +++ b/iOS/IconView.swift @@ -17,18 +17,18 @@ final class IconView: UIView { if self.traitCollection.userInterfaceStyle == .dark { if self.iconImage?.isDark ?? false { - self.isDisconcernable = false + self.isDiscernable = false self.setNeedsLayout() } else { - self.isDisconcernable = true + self.isDiscernable = true self.setNeedsLayout() } } else { if self.iconImage?.isBright ?? false { - self.isDisconcernable = false + self.isDiscernable = false self.setNeedsLayout() } else { - self.isDisconcernable = true + self.isDiscernable = true self.setNeedsLayout() } } @@ -37,7 +37,7 @@ final class IconView: UIView { } } - private var isDisconcernable = true + private var isDiscernable = true private let imageView: UIImageView = { let imageView = NonIntrinsicImageView(image: AppAssets.faviconTemplateImage) @@ -79,7 +79,7 @@ final class IconView: UIView { override func layoutSubviews() { imageView.setFrameIfNotEqual(rectForImageView()) - if !isBackgroundSuppressed && ((iconImage != nil && isVerticalBackgroundExposed) || !isDisconcernable) { + if !isBackgroundSuppressed && ((iconImage != nil && isVerticalBackgroundExposed) || !isDiscernable) { backgroundColor = AppAssets.iconBackgroundColor } else { backgroundColor = nil From c9807a6bb05d5ebcd4057529b99ab37d4b7b14b8 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 7 Mar 2021 13:03:28 -0600 Subject: [PATCH 15/35] Update to the latest RSCore. Fixes #2685 --- .../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 71a837402..ae2eb2544 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -60,8 +60,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/RSCore.git", "state": { "branch": null, - "revision": "6b2ef2580968905af825c40442dc0ba3126032c0", - "version": "1.0.2" + "revision": "7070f8a7f5cad5fcbe2444a4b6265af20efd85a6", + "version": "1.0.3" } }, { From 64e5a09ac6938b3041d1acfc9316b10b202e9191 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 9 Mar 2021 04:23:19 -0600 Subject: [PATCH 16/35] Return to main queue after getting notified of the dispatch group completion. Fixes #2863 --- .../CloudKit/CloudKitAccountDelegate.swift | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index 15aeebfeb..1fbc494b5 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -310,20 +310,22 @@ final class CloudKitAccountDelegate: AccountDelegate { } group.notify(queue: DispatchQueue.global(qos: .background)) { - guard !errorOccurred else { - self.refreshProgress.completeTask() - completion(.failure(CloudKitAccountDelegateError.unknown)) - return - } - - self.accountZone.removeFolder(folder) { result in - self.refreshProgress.completeTask() - switch result { - case .success: - account.removeFolder(folder) - completion(.success(())) - case .failure(let error): - completion(.failure(error)) + DispatchQueue.main.async { + guard !errorOccurred else { + self.refreshProgress.completeTask() + completion(.failure(CloudKitAccountDelegateError.unknown)) + return + } + + self.accountZone.removeFolder(folder) { result in + self.refreshProgress.completeTask() + switch result { + case .success: + account.removeFolder(folder) + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } } } } From 3132a2c77907a29687264e1aaf0bf5237c97b54d Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 9 Mar 2021 04:47:15 -0600 Subject: [PATCH 17/35] Validate server data before accessing it using a subscript. Fixes #2861 --- Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift index 1d4845fdb..4e9f1b0c1 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift @@ -117,7 +117,9 @@ final class ReaderAPICaller: NSObject { var authData: [String: String] = [:] rawData.split(separator: "\n").forEach({ (line: Substring) in let items = line.split(separator: "=").map{String($0)} - authData[items[0]] = items[1] + if items.count == 2 { + authData[items[0]] = items[1] + } }) guard let authString = authData["Auth"] else { From bffd341992f95a48cd4107d13d2935b755a59e0b Mon Sep 17 00:00:00 2001 From: Andrew Brehaut Date: Thu, 11 Mar 2021 08:16:51 +1300 Subject: [PATCH 18/35] #2371 Checks footnote target before overriding browser default behavior --- Shared/Article Rendering/newsfoot.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Shared/Article Rendering/newsfoot.js b/Shared/Article Rendering/newsfoot.js index 296c74d0b..fc5b386d5 100644 --- a/Shared/Article Rendering/newsfoot.js +++ b/Shared/Article Rendering/newsfoot.js @@ -140,12 +140,17 @@ if (targetId) break; } if (targetId === undefined) return; - + + // Only override the default behaviour when we know we can find the + // target element + const targetElement = document.querySelector(`[id='${targetId}']`); + if (targetElement === null) return; + ev.preventDefault(); installContainer(ev.target); - const content = document.querySelector(`[id='${targetId}']`).innerHTML; - void new Footnote(content, ev.target); + + void new Footnote(targetElement.innerHTML, ev.target); }); // Handle clicks on the footnote reverse link From bd71b5d79a567ce9e2117e6a4668ad1bfbd59b65 Mon Sep 17 00:00:00 2001 From: Andrew Brehaut Date: Thu, 11 Mar 2021 10:54:25 +1300 Subject: [PATCH 19/35] Update Shared/Article Rendering/newsfoot.js Good catch thanks Co-authored-by: Jed Fox --- Shared/Article Rendering/newsfoot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/Article Rendering/newsfoot.js b/Shared/Article Rendering/newsfoot.js index fc5b386d5..1a53dbe42 100644 --- a/Shared/Article Rendering/newsfoot.js +++ b/Shared/Article Rendering/newsfoot.js @@ -143,7 +143,7 @@ // Only override the default behaviour when we know we can find the // target element - const targetElement = document.querySelector(`[id='${targetId}']`); + const targetElement = document.getElementById(targetId); if (targetElement === null) return; ev.preventDefault(); From 9dc9db597a7f04a79598e2947973cd0de4700f8e Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 11 Mar 2021 19:29:52 -0600 Subject: [PATCH 20/35] Upgrade to the latest RSCore --- .../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 ae2eb2544..0be93847a 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -60,8 +60,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/RSCore.git", "state": { "branch": null, - "revision": "7070f8a7f5cad5fcbe2444a4b6265af20efd85a6", - "version": "1.0.3" + "revision": "09bdc9af601af2ca6a3a72a8b3c6aec04dfdbd88", + "version": "1.0.4" } }, { From 7e791a2e7dfc1fcd5a436c08f9324390f9736d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiel=20Gillard=20=F0=9F=A4=AA?= Date: Tue, 23 Mar 2021 17:00:54 +1100 Subject: [PATCH 21/35] Feedly API is no longer returning the feed values for deleted feeds, so make the response (which we ignore anyway), optional. Fixes #2897 --- Account/Sources/Account/Feedly/FeedlyAPICaller.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift index 36f974835..f855a95e0 100644 --- a/Account/Sources/Account/Feedly/FeedlyAPICaller.swift +++ b/Account/Sources/Account/Feedly/FeedlyAPICaller.swift @@ -369,7 +369,9 @@ final class FeedlyAPICaller { } } - send(request: request, resultType: [FeedlyFeed].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in + // `resultType` is optional because the Feedly API has gone from returning an array of removed feeds to returning `null`. + // https://developer.feedly.com/v3/collections/#remove-multiple-feeds-from-a-personal-collection + send(request: request, resultType: Optional<[FeedlyFeed]>.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success((let httpResponse, _)): if httpResponse.statusCode == 200 { From 9c761c80df18ba83337a3d713e5056bf75f50c59 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 24 Mar 2021 05:43:07 -0500 Subject: [PATCH 22/35] Add an optional limit parameter to the smart feeds. Fixes #2627 --- Account/Sources/Account/Account.swift | 68 +++++++++---------- .../ArticlesDatabase/ArticlesDatabase.swift | 24 +++---- .../ArticlesDatabase/ArticlesTable.swift | 45 +++++++----- Shared/SmartFeeds/StarredFeedDelegate.swift | 2 +- Shared/SmartFeeds/TodayFeedDelegate.swift | 2 +- Shared/SmartFeeds/UnreadFeed.swift | 2 +- 6 files changed, 76 insertions(+), 67 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 7acf4816b..6b0cd940c 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -53,9 +53,9 @@ public enum AccountType: Int, Codable { } public enum FetchType { - case starred - case unread - case today + case starred(Int?) + case unread(Int?) + case today(Int?) case folder(Folder, Bool) case webFeed(WebFeed) case articleIDs(Set) @@ -674,12 +674,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func fetchArticles(_ fetchType: FetchType) throws -> Set
{ switch fetchType { - case .starred: - return try fetchStarredArticles() - case .unread: - return try fetchUnreadArticles() - case .today: - return try fetchTodayArticles() + case .starred(let limit): + return try fetchStarredArticles(limit: limit) + case .unread(let limit): + return try fetchUnreadArticles(limit: limit) + case .today(let limit): + return try fetchTodayArticles(limit: limit) case .folder(let folder, let readFilter): if readFilter { return try fetchUnreadArticles(folder: folder) @@ -699,12 +699,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func fetchArticlesAsync(_ fetchType: FetchType, _ completion: @escaping ArticleSetResultBlock) { switch fetchType { - case .starred: - fetchStarredArticlesAsync(completion) - case .unread: - fetchUnreadArticlesAsync(completion) - case .today: - fetchTodayArticlesAsync(completion) + case .starred(let limit): + fetchStarredArticlesAsync(limit: limit, completion) + case .unread(let limit): + fetchUnreadArticlesAsync(limit: limit, completion) + case .today(let limit): + fetchTodayArticlesAsync(limit: limit, completion) case .folder(let folder, let readFilter): if readFilter { return fetchUnreadArticlesAsync(folder: folder, completion) @@ -1046,28 +1046,28 @@ extension Account: WebFeedMetadataDelegate { private extension Account { - func fetchStarredArticles() throws -> Set
{ - return try database.fetchStarredArticles(flattenedWebFeeds().webFeedIDs()) + func fetchStarredArticles(limit: Int?) throws -> Set
{ + return try database.fetchStarredArticles(flattenedWebFeeds().webFeedIDs(), limit) } - func fetchStarredArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { - database.fetchedStarredArticlesAsync(flattenedWebFeeds().webFeedIDs(), completion) + func fetchStarredArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + database.fetchedStarredArticlesAsync(flattenedWebFeeds().webFeedIDs(), limit, completion) } - func fetchUnreadArticles() throws -> Set
{ - return try fetchUnreadArticles(forContainer: self) + func fetchUnreadArticles(limit: Int?) throws -> Set
{ + return try fetchUnreadArticles(forContainer: self, limit: limit) } - func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { - fetchUnreadArticlesAsync(forContainer: self, completion) + func fetchUnreadArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + fetchUnreadArticlesAsync(forContainer: self, limit: limit, completion) } - func fetchTodayArticles() throws -> Set
{ - return try database.fetchTodayArticles(flattenedWebFeeds().webFeedIDs()) + func fetchTodayArticles(limit: Int?) throws -> Set
{ + return try database.fetchTodayArticles(flattenedWebFeeds().webFeedIDs(), limit) } - func fetchTodayArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { - database.fetchTodayArticlesAsync(flattenedWebFeeds().webFeedIDs(), completion) + func fetchTodayArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + database.fetchTodayArticlesAsync(flattenedWebFeeds().webFeedIDs(), limit, completion) } func fetchArticles(folder: Folder) throws -> Set
{ @@ -1079,11 +1079,11 @@ private extension Account { } func fetchUnreadArticles(folder: Folder) throws -> Set
{ - return try fetchUnreadArticles(forContainer: folder) + return try fetchUnreadArticles(forContainer: folder, limit: nil) } func fetchUnreadArticlesAsync(folder: Folder, _ completion: @escaping ArticleSetResultBlock) { - fetchUnreadArticlesAsync(forContainer: folder, completion) + fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion) } func fetchArticles(webFeed: WebFeed) throws -> Set
{ @@ -1129,7 +1129,7 @@ private extension Account { } func fetchUnreadArticles(webFeed: WebFeed) throws -> Set
{ - let articles = try database.fetchUnreadArticles(Set([webFeed.webFeedID])) + let articles = try database.fetchUnreadArticles(Set([webFeed.webFeedID]), nil) validateUnreadCount(webFeed, articles) return articles } @@ -1154,16 +1154,16 @@ private extension Account { } } - func fetchUnreadArticles(forContainer container: Container) throws -> Set
{ + func fetchUnreadArticles(forContainer container: Container, limit: Int?) throws -> Set
{ let feeds = container.flattenedWebFeeds() - let articles = try database.fetchUnreadArticles(feeds.webFeedIDs()) + let articles = try database.fetchUnreadArticles(feeds.webFeedIDs(), limit) validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) return articles } - func fetchUnreadArticlesAsync(forContainer container: Container, _ completion: @escaping ArticleSetResultBlock) { + func fetchUnreadArticlesAsync(forContainer container: Container, limit: Int?, _ completion: @escaping ArticleSetResultBlock) { let webFeeds = container.flattenedWebFeeds() - database.fetchUnreadArticlesAsync(webFeeds.webFeedIDs()) { [weak self] (articleSetResult) in + database.fetchUnreadArticlesAsync(webFeeds.webFeedIDs(), limit) { [weak self] (articleSetResult) in switch articleSetResult { case .success(let articles): self?.validateUnreadCountsAfterFetchingUnreadArticles(webFeeds, articles) diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift index ddb576ba9..a8e05eea2 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift @@ -102,16 +102,16 @@ public final class ArticlesDatabase { return try articlesTable.fetchArticles(articleIDs: articleIDs) } - public func fetchUnreadArticles(_ webFeedIDs: Set) throws -> Set
{ - return try articlesTable.fetchUnreadArticles(webFeedIDs) + public func fetchUnreadArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ + return try articlesTable.fetchUnreadArticles(webFeedIDs, limit) } - public func fetchTodayArticles(_ webFeedIDs: Set) throws -> Set
{ - return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate()) + public func fetchTodayArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ + return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate(), limit) } - public func fetchStarredArticles(_ webFeedIDs: Set) throws -> Set
{ - return try articlesTable.fetchStarredArticles(webFeedIDs) + public func fetchStarredArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ + return try articlesTable.fetchStarredArticles(webFeedIDs, limit) } public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set) throws -> Set
{ @@ -136,16 +136,16 @@ public final class ArticlesDatabase { articlesTable.fetchArticlesAsync(articleIDs: articleIDs, completion) } - public func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { - articlesTable.fetchUnreadArticlesAsync(webFeedIDs, completion) + public func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + articlesTable.fetchUnreadArticlesAsync(webFeedIDs, limit, completion) } - public func fetchTodayArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { - articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), completion) + public func fetchTodayArticlesAsync(_ webFeedIDs: Set, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), limit, completion) } - public func fetchedStarredArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { - articlesTable.fetchStarredArticlesAsync(webFeedIDs, completion) + public func fetchedStarredArticlesAsync(_ webFeedIDs: Set, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + articlesTable.fetchStarredArticlesAsync(webFeedIDs, limit, completion) } public func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift index 61c2845ef..9518a6b84 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift @@ -75,32 +75,32 @@ final class ArticlesTable: DatabaseTable { // MARK: - Fetching Unread Articles - func fetchUnreadArticles(_ webFeedIDs: Set) throws -> Set
{ - return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, $0) } + func fetchUnreadArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ + return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, limit, $0) } } - func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { - fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, $0) }, completion) + func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, limit, $0) }, completion) } // MARK: - Fetching Today Articles - func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date) throws -> Set
{ - return try fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) } + func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date, _ limit: Int?) throws -> Set
{ + return try fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, limit, $0) } } - func fetchArticlesSinceAsync(_ webFeedIDs: Set, _ cutoffDate: Date, _ completion: @escaping ArticleSetResultBlock) { - fetchArticlesAsync({ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) }, completion) + func fetchArticlesSinceAsync(_ webFeedIDs: Set, _ cutoffDate: Date, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + fetchArticlesAsync({ self.fetchArticlesSince(webFeedIDs, cutoffDate, limit, $0) }, completion) } // MARK: - Fetching Starred Articles - func fetchStarredArticles(_ webFeedIDs: Set) throws -> Set
{ - return try fetchArticles{ self.fetchStarredArticles(webFeedIDs, $0) } + func fetchStarredArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ + return try fetchArticles{ self.fetchStarredArticles(webFeedIDs, limit, $0) } } - func fetchStarredArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { - fetchArticlesAsync({ self.fetchStarredArticles(webFeedIDs, $0) }, completion) + func fetchStarredArticlesAsync(_ webFeedIDs: Set, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + fetchArticlesAsync({ self.fetchStarredArticles(webFeedIDs, limit, $0) }, completion) } // MARK: - Fetching Search Articles @@ -819,14 +819,17 @@ private extension ArticlesTable { return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } - func fetchUnreadArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ + func fetchUnreadArticles(_ webFeedIDs: Set, _ limit: Int?, _ database: FMDatabase) -> Set
{ // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0 if webFeedIDs.isEmpty { return Set
() } let parameters = webFeedIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - let whereClause = "feedID in \(placeholders) and read=0" + var whereClause = "feedID in \(placeholders) and read=0" + if let limit = limit { + whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)") + } return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } @@ -844,7 +847,7 @@ private extension ArticlesTable { return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } - func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date, _ database: FMDatabase) -> Set
{ + func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date, _ limit: Int?, _ database: FMDatabase) -> Set
{ // 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. @@ -853,18 +856,24 @@ private extension ArticlesTable { } let parameters = webFeedIDs.map { $0 as AnyObject } + [cutoffDate as AnyObject, cutoffDate as AnyObject] let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?))" + var whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?))" + if let limit = limit { + whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)") + } return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } - func fetchStarredArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ + func fetchStarredArticles(_ webFeedIDs: Set, _ limit: Int?, _ database: FMDatabase) -> Set
{ // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred=1; if webFeedIDs.isEmpty { return Set
() } let parameters = webFeedIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - let whereClause = "feedID in \(placeholders) and starred=1" + var whereClause = "feedID in \(placeholders) and starred=1" + if let limit = limit { + whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)") + } return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } diff --git a/Shared/SmartFeeds/StarredFeedDelegate.swift b/Shared/SmartFeeds/StarredFeedDelegate.swift index 251e72cd5..55770fe36 100644 --- a/Shared/SmartFeeds/StarredFeedDelegate.swift +++ b/Shared/SmartFeeds/StarredFeedDelegate.swift @@ -21,7 +21,7 @@ struct StarredFeedDelegate: SmartFeedDelegate { } let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title") - let fetchType: FetchType = .starred + let fetchType: FetchType = .starred(nil) var smallIcon: IconImage? { return AppAssets.starredFeedImage } diff --git a/Shared/SmartFeeds/TodayFeedDelegate.swift b/Shared/SmartFeeds/TodayFeedDelegate.swift index 085a43777..ad6e47977 100644 --- a/Shared/SmartFeeds/TodayFeedDelegate.swift +++ b/Shared/SmartFeeds/TodayFeedDelegate.swift @@ -19,7 +19,7 @@ struct TodayFeedDelegate: SmartFeedDelegate { } let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title") - let fetchType = FetchType.today + let fetchType = FetchType.today(nil) var smallIcon: IconImage? { return AppAssets.todayFeedImage } diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index 4ef22635b..f8ce3660c 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -29,7 +29,7 @@ final class UnreadFeed: PseudoFeed { } let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title") - let fetchType = FetchType.unread + let fetchType = FetchType.unread(nil) var unreadCount = 0 { didSet { From 7f702abc8a05c06ba79aa0948ade296eb5cdac97 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 24 Mar 2021 20:06:48 +0800 Subject: [PATCH 23/35] fixes build error --- Account/Sources/Account/Account.swift | 6 +++--- iOS/MasterFeed/MasterFeedViewController.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 6b0cd940c..d82023c69 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -53,9 +53,9 @@ public enum AccountType: Int, Codable { } public enum FetchType { - case starred(Int?) - case unread(Int?) - case today(Int?) + case starred(_: Int? = nil) + case unread(_: Int? = nil) + case today(_: Int? = nil) case folder(Folder, Bool) case webFeed(WebFeed) case articleIDs(Set) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index c2f719e6e..ee332f0a0 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -1235,7 +1235,7 @@ private extension MasterFeedViewController { let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, account.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? account.fetchArticles(.unread) { + if let articles = try? account.fetchArticles(.unread()) { self?.coordinator.markAllAsRead(Array(articles)) } } From ca45ea6e0567857323ec9386aa6545a24e03df95 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Wed, 24 Mar 2021 20:30:21 +0800 Subject: [PATCH 24/35] Widget now uses limits --- Shared/Widget/WidgetDataEncoder.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 5b928cea3..8a96d3737 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -12,11 +12,13 @@ import os.log import UIKit import RSCore import Articles +import Account public final class WidgetDataEncoder { private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") + private let fetchLimit = 7 private var backgroundTaskID: UIBackgroundTaskIdentifier! private lazy var appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String @@ -31,11 +33,9 @@ public final class WidgetDataEncoder { os_log(.debug, log: log, "Starting encoding widget data.") do { - let unreadArticles = Array(try SmartFeedsController.shared.unreadFeed.fetchArticles()).sortedByDate(.orderedDescending) - - let starredArticles = Array(try SmartFeedsController.shared.starredFeed.fetchArticles()).sortedByDate(.orderedDescending) - - let todayArticles = Array(try SmartFeedsController.shared.todayFeed.fetchUnreadArticles()).sortedByDate(.orderedDescending) + 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]() @@ -74,9 +74,9 @@ public final class WidgetDataEncoder { if today.count == 7 { break } } - let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount, - currentTodayCount: try! SmartFeedsController.shared.todayFeed.fetchUnreadArticles().count, - currentStarredCount: try! SmartFeedsController.shared.starredFeed.fetchArticles().count, + let latestData = WidgetData(currentUnreadCount: try! AccountManager.shared.fetchArticles(.unread()).count, + currentTodayCount: try! AccountManager.shared.fetchArticles(.today()).count, + currentStarredCount: try! AccountManager.shared.fetchArticles(.starred()).count, unreadArticles: unread, starredArticles: starred, todayArticles:today, From 34de76009bf50a40110b55b6ebb5f2fc325520fa Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 24 Mar 2021 16:50:35 -0500 Subject: [PATCH 25/35] Update to latest RSCore --- .../Account/CloudKit/CloudKitAccountDelegate.swift | 2 +- .../Account/CloudKit/CloudKitAccountZone.swift | 12 ++++++------ .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index 1fbc494b5..660b22939 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -199,7 +199,7 @@ final class CloudKitAccountDelegate: AccountDelegate { completion(.success(())) case .failure(let error): switch error { - case CloudKitZoneError.invalidParameter: + case CloudKitZoneError.corruptAccount: // We got into a bad state and should remove the feed to clear up the bad data account.clearWebFeedMetadata(feed) container.removeWebFeed(feed) diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift index 64c99794f..fa58df337 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift @@ -103,7 +103,7 @@ final class CloudKitAccountZone: CloudKitZone { } guard let containerExternalID = container.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } record[CloudKitWebFeed.Fields.containerExternalIDs] = [containerExternalID] @@ -121,7 +121,7 @@ final class CloudKitAccountZone: CloudKitZone { /// Rename the given web feed func renameWebFeed(_ webFeed: WebFeed, editedName: String?, completion: @escaping (Result) -> Void) { guard let externalID = webFeed.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } @@ -142,7 +142,7 @@ final class CloudKitAccountZone: CloudKitZone { /// Removes a web feed from a container and optionally deletes it, calling the completion with true if deleted func removeWebFeed(_ webFeed: WebFeed, from: Container, completion: @escaping (Result) -> Void) { guard let fromContainerExternalID = from.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } @@ -187,7 +187,7 @@ final class CloudKitAccountZone: CloudKitZone { func moveWebFeed(_ webFeed: WebFeed, from: Container, to: Container, completion: @escaping (Result) -> Void) { guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } @@ -209,7 +209,7 @@ final class CloudKitAccountZone: CloudKitZone { func addWebFeed(_ webFeed: WebFeed, to: Container, completion: @escaping (Result) -> Void) { guard let toContainerExternalID = to.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } @@ -292,7 +292,7 @@ final class CloudKitAccountZone: CloudKitZone { func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result) -> Void) { guard let externalID = folder.externalID else { - completion(.failure(CloudKitZoneError.invalidParameter)) + completion(.failure(CloudKitZoneError.corruptAccount)) return } diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0be93847a..d98ef98fd 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -60,8 +60,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/RSCore.git", "state": { "branch": null, - "revision": "09bdc9af601af2ca6a3a72a8b3c6aec04dfdbd88", - "version": "1.0.4" + "revision": "665319af9428455e45c1d043156db85548b73f31", + "version": "1.0.5" } }, { @@ -96,8 +96,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/RSWeb.git", "state": { "branch": null, - "revision": "2f7bc7671a751e994e2567c8221ba64e884d5583", - "version": "1.0.0" + "revision": "2f9ad98736c5c17dfb2be0b3cc06e71a49b061fa", + "version": "1.0.1" } }, { From b5eff641d02faecfe8bee346d5a5855310d93370 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 25 Mar 2021 16:07:22 -0500 Subject: [PATCH 26/35] Change so that we don't validate and change unread counts for unread limit queries --- Account/Sources/Account/Account.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index d82023c69..a8d62e856 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -1157,7 +1157,13 @@ private extension Account { func fetchUnreadArticles(forContainer container: Container, limit: Int?) throws -> Set
{ let feeds = container.flattenedWebFeeds() let articles = try database.fetchUnreadArticles(feeds.webFeedIDs(), limit) - validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) + + // We don't validate limit queries because they, by definition, won't correctly match the + // complete unread state for the given container. + if limit == nil { + validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) + } + return articles } @@ -1166,7 +1172,13 @@ private extension Account { database.fetchUnreadArticlesAsync(webFeeds.webFeedIDs(), limit) { [weak self] (articleSetResult) in switch articleSetResult { case .success(let articles): - self?.validateUnreadCountsAfterFetchingUnreadArticles(webFeeds, articles) + + // We don't validate limit queries because they, by definition, won't correctly match the + // complete unread state for the given container. + if limit == nil { + self?.validateUnreadCountsAfterFetchingUnreadArticles(webFeeds, articles) + } + completion(.success(articles)) case .failure(let databaseError): completion(.failure(databaseError)) From 3c2c17df0dc99835856b71a1e4941de5296a598b Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 25 Mar 2021 16:28:15 -0500 Subject: [PATCH 27/35] Fix threading issue --- .../MasterTimelineViewController.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index d92638b33..27462c202 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -471,13 +471,15 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } @objc func userDefaultsDidChange(_ note: Notification) { - if numberOfTextLines != AppDefaults.shared.timelineNumberOfLines || iconSize != AppDefaults.shared.timelineIconSize { - numberOfTextLines = AppDefaults.shared.timelineNumberOfLines - iconSize = AppDefaults.shared.timelineIconSize - resetEstimatedRowHeight() - reloadAllVisibleCells() + DispatchQueue.main.async { + if self.numberOfTextLines != AppDefaults.shared.timelineNumberOfLines || self.iconSize != AppDefaults.shared.timelineIconSize { + self.numberOfTextLines = AppDefaults.shared.timelineNumberOfLines + self.iconSize = AppDefaults.shared.timelineIconSize + self.resetEstimatedRowHeight() + self.reloadAllVisibleCells() + } + self.updateToolbar() } - updateToolbar() } @objc func contentSizeCategoryDidChange(_ note: Notification) { From 1ca0df67a4adfa2457170c06c0a2398f21122487 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Fri, 26 Mar 2021 11:36:20 +0800 Subject: [PATCH 28/35] widget and unread badge counts are correct also - counts in the widget revert back to using data available in the SmartFeedsController. --- Shared/Widget/WidgetDataEncoder.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 8a96d3737..86c46be3b 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -49,7 +49,6 @@ public final class WidgetDataEncoder { feedIcon: article.iconImage()?.image.dataRepresentation(), pubDate: article.datePublished?.description ?? "") unread.append(latestArticle) - if unread.count == 7 { break } } for article in starredArticles { @@ -60,7 +59,6 @@ public final class WidgetDataEncoder { feedIcon: article.iconImage()?.image.dataRepresentation(), pubDate: article.datePublished?.description ?? "") starred.append(latestArticle) - if starred.count == 7 { break } } for article in todayArticles { @@ -71,12 +69,11 @@ public final class WidgetDataEncoder { feedIcon: article.iconImage()?.image.dataRepresentation(), pubDate: article.datePublished?.description ?? "") today.append(latestArticle) - if today.count == 7 { break } } - let latestData = WidgetData(currentUnreadCount: try! AccountManager.shared.fetchArticles(.unread()).count, - currentTodayCount: try! AccountManager.shared.fetchArticles(.today()).count, - currentStarredCount: try! AccountManager.shared.fetchArticles(.starred()).count, + 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, From c35dabbc552b1f8a079b2daa383d01c478b51f37 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 27 Mar 2021 19:12:30 -0500 Subject: [PATCH 29/35] Change so that we don't join with the articles table when finding starred or unread article ids. Fixes #2915 --- Account/Sources/Account/Account.swift | 4 +- .../ArticlesDatabase/ArticlesDatabase.swift | 12 ++--- .../ArticlesDatabase/ArticlesTable.swift | 48 ++----------------- .../ArticlesDatabase/StatusesTable.swift | 32 +++++++++++++ 4 files changed, 44 insertions(+), 52 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index a8d62e856..e1606314f 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -731,11 +731,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } public func fetchUnreadArticleIDs(_ completion: @escaping ArticleIDsCompletionBlock) { - database.fetchUnreadArticleIDsAsync(webFeedIDs: flattenedWebFeeds().webFeedIDs(), completion: completion) + database.fetchUnreadArticleIDsAsync(completion: completion) } public func fetchStarredArticleIDs(_ completion: @escaping ArticleIDsCompletionBlock) { - database.fetchStarredArticleIDsAsync(webFeedIDs: flattenedWebFeeds().webFeedIDs(), completion: completion) + database.fetchStarredArticleIDsAsync(completion: completion) } /// Fetch articleIDs for articles that we should have, but don’t. These articles are either (starred) or (newer than the article cutoff date). diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift index a8e05eea2..5a7355201 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift @@ -222,14 +222,14 @@ public final class ArticlesDatabase { // MARK: - Status - /// Fetch the articleIDs of unread articles in feeds specified by webFeedIDs. - public func fetchUnreadArticleIDsAsync(webFeedIDs: Set, completion: @escaping ArticleIDsCompletionBlock) { - articlesTable.fetchUnreadArticleIDsAsync(webFeedIDs, completion) + /// Fetch the articleIDs of unread articles. + public func fetchUnreadArticleIDsAsync(completion: @escaping ArticleIDsCompletionBlock) { + articlesTable.fetchUnreadArticleIDsAsync(completion) } - /// Fetch the articleIDs of starred articles in feeds specified by webFeedIDs. - public func fetchStarredArticleIDsAsync(webFeedIDs: Set, completion: @escaping ArticleIDsCompletionBlock) { - articlesTable.fetchStarredArticleIDsAsync(webFeedIDs, completion) + /// Fetch the articleIDs of starred articles. + public func fetchStarredArticleIDsAsync(completion: @escaping ArticleIDsCompletionBlock) { + articlesTable.fetchStarredArticleIDsAsync(completion) } /// Fetch articleIDs for articles that we should have, but don’t. These articles are either (starred) or (newer than the article cutoff date). diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift index 9518a6b84..423f61c7e 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift @@ -418,12 +418,12 @@ final class ArticlesTable: DatabaseTable { // MARK: - Statuses - func fetchUnreadArticleIDsAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleIDsCompletionBlock) { - fetchArticleIDsAsync(.read, false, webFeedIDs, completion) + func fetchUnreadArticleIDsAsync(_ completion: @escaping ArticleIDsCompletionBlock) { + statusesTable.fetchArticleIDsAsync(.read, false, completion) } - func fetchStarredArticleIDsAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleIDsCompletionBlock) { - fetchArticleIDsAsync(.starred, true, webFeedIDs, completion) + func fetchStarredArticleIDsAsync(_ completion: @escaping ArticleIDsCompletionBlock) { + statusesTable.fetchArticleIDsAsync(.starred, true, completion) } func fetchStarredArticleIDs() throws -> Set { @@ -768,46 +768,6 @@ private extension ArticlesTable { return articlesWithResultSet(resultSet, database) } - func fetchArticleIDsAsync(_ statusKey: ArticleStatus.Key, _ value: Bool, _ webFeedIDs: Set, _ completion: @escaping ArticleIDsCompletionBlock) { - guard !webFeedIDs.isEmpty else { - completion(.success(Set())) - return - } - - queue.runInDatabase { databaseResult in - - func makeDatabaseCalls(_ database: FMDatabase) { - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - var sql = "select articleID from articles natural join statuses where feedID in \(placeholders) and \(statusKey.rawValue)=" - sql += value ? "1" : "0" - sql += ";" - - let parameters = Array(webFeedIDs) as [Any] - - guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else { - DispatchQueue.main.async { - completion(.success(Set())) - } - return - } - - let articleIDs = resultSet.mapToSet{ $0.string(forColumnIndex: 0) } - DispatchQueue.main.async { - completion(.success(articleIDs)) - } - } - - switch databaseResult { - case .success(let database): - makeDatabaseCalls(database) - case .failure(let databaseError): - DispatchQueue.main.async { - completion(.failure(databaseError)) - } - } - } - } - func fetchArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0 if webFeedIDs.isEmpty { diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift index bf5e59d99..bb53a36a2 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/StatusesTable.swift @@ -102,6 +102,38 @@ final class StatusesTable: DatabaseTable { return try fetchArticleIDs("select articleID from statuses where starred=1;") } + func fetchArticleIDsAsync(_ statusKey: ArticleStatus.Key, _ value: Bool, _ completion: @escaping ArticleIDsCompletionBlock) { + queue.runInDatabase { databaseResult in + + func makeDatabaseCalls(_ database: FMDatabase) { + var sql = "select articleID from statuses where \(statusKey.rawValue)=" + sql += value ? "1" : "0" + sql += ";" + + guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else { + DispatchQueue.main.async { + completion(.success(Set())) + } + return + } + + let articleIDs = resultSet.mapToSet{ $0.string(forColumnIndex: 0) } + DispatchQueue.main.async { + completion(.success(articleIDs)) + } + } + + switch databaseResult { + case .success(let database): + makeDatabaseCalls(database) + case .failure(let databaseError): + DispatchQueue.main.async { + completion(.failure(databaseError)) + } + } + } + } + func fetchArticleIDsForStatusesWithoutArticlesNewerThan(_ cutoffDate: Date, _ completion: @escaping ArticleIDsCompletionBlock) { queue.runInDatabase { databaseResult in From 022e4ae1536af58d2c9d842270ede52ddf5bdac2 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 27 Mar 2021 19:18:22 -0500 Subject: [PATCH 30/35] Fail when trying to add a feed that isn't parsable. Fixes #2921 --- .../Account/CloudKit/CloudKitAccountDelegate.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index 660b22939..5e2ece0d8 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -668,7 +668,6 @@ private extension CloudKitAccountDelegate { } func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, completion: @escaping (Result) -> Void) { - BatchUpdate.shared.start() refreshProgress.addToNumberOfTasksAndRemaining(5) FeedFinder.find(url: url) { result in @@ -676,14 +675,12 @@ private extension CloudKitAccountDelegate { switch result { case .success(let feedSpecifiers): guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), let url = URL(string: bestFeedSpecifier.urlString) else { - BatchUpdate.shared.end() self.refreshProgress.completeTasks(4) completion(.failure(AccountError.createErrorNotFound)) return } if account.hasWebFeed(withURL: bestFeedSpecifier.urlString) { - BatchUpdate.shared.end() self.refreshProgress.completeTasks(4) completion(.failure(AccountError.createErrorAlreadySubscribed)) return @@ -700,7 +697,6 @@ private extension CloudKitAccountDelegate { account.update(feed, with: parsedFeed) { result in switch result { case .success: - BatchUpdate.shared.end() self.accountZone.createWebFeed(url: bestFeedSpecifier.urlString, name: parsedFeed.title, @@ -724,21 +720,21 @@ private extension CloudKitAccountDelegate { } case .failure(let error): - BatchUpdate.shared.end() + container.removeWebFeed(feed) self.refreshProgress.completeTasks(3) completion(.failure(error)) } } } else { - self.refreshProgress.completeTasks(4) - completion(.success(feed)) + container.removeWebFeed(feed) + self.refreshProgress.completeTasks(3) + completion(.failure(AccountError.createErrorNotFound)) } } case .failure: - BatchUpdate.shared.end() self.refreshProgress.completeTasks(4) completion(.failure(AccountError.createErrorNotFound)) } From ce0d5590f164eea5f7d5655d149d3be0a66193ab Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 29 Mar 2021 14:48:48 -0500 Subject: [PATCH 31/35] Send out an name change notification if the feed name changes. Fixes #2939 --- Account/Sources/Account/WebFeed.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Account/Sources/Account/WebFeed.swift b/Account/Sources/Account/WebFeed.swift index d49e7a938..9fddbb5eb 100644 --- a/Account/Sources/Account/WebFeed.swift +++ b/Account/Sources/Account/WebFeed.swift @@ -77,7 +77,13 @@ public final class WebFeed: Feed, Renamable, Hashable { } } - public var name: String? + public var name: String? { + didSet { + if name != oldValue { + postDisplayNameDidChangeNotification() + } + } + } public var authors: Set? { get { From d2245b629bffe71ae873221cbdaf4e099d3aa426 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 29 Mar 2021 18:53:49 -0500 Subject: [PATCH 32/35] Add locale to date formatter --- .../Account/FeedProvider/Twitter/TwitterFeedProvider.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterFeedProvider.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterFeedProvider.swift index 9a3046103..a2f477385 100644 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterFeedProvider.swift +++ b/Account/Sources/Account/FeedProvider/Twitter/TwitterFeedProvider.swift @@ -399,6 +399,7 @@ private extension TwitterFeedProvider { let decoder = JSONDecoder() let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.init(identifier: "en_US_POSIX") dateFormatter.dateFormat = Self.dateFormat decoder.dateDecodingStrategy = .formatted(dateFormatter) From 0d5de9c3255cae41d31a2c544d1f64318a51ac93 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 3 Apr 2021 09:06:51 -0500 Subject: [PATCH 33/35] Renamed Open in Safari activity to Open in Browser --- iOS/Article/OpenInSafariActivity.swift | 8 ++++---- iOS/Article/WebViewController.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/iOS/Article/OpenInSafariActivity.swift b/iOS/Article/OpenInSafariActivity.swift index 9b5b5ed8f..2c9ae9eab 100644 --- a/iOS/Article/OpenInSafariActivity.swift +++ b/iOS/Article/OpenInSafariActivity.swift @@ -1,5 +1,5 @@ // -// OpenInSafariActivity.swift +// OpenInBrowserActivity.swift // NetNewsWire-iOS // // Created by Maurice Parker on 1/9/20. @@ -8,16 +8,16 @@ import UIKit -class OpenInSafariActivity: UIActivity { +class OpenInBrowserActivity: UIActivity { private var activityItems: [Any]? override var activityTitle: String? { - return NSLocalizedString("Open in Safari", comment: "Open in Safari") + return NSLocalizedString("Open in Browser", comment: "Open in Browser") } override var activityImage: UIImage? { - return UIImage(systemName: "safari", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)) + return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)) } override var activityType: UIActivity.ActivityType? { diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 6024d02dd..18486efc3 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -239,7 +239,7 @@ class WebViewController: UIViewController { return } - let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInSafariActivity()]) + let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()]) activityViewController.popoverPresentationController?.barButtonItem = popOverBarButtonItem present(activityViewController, animated: true) } From 1874e0c7d2f8eb76c2942bf686f2d40004fca5f9 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 3 Apr 2021 10:40:46 -0500 Subject: [PATCH 34/35] Change the luminance algorithm so that we don't miss images in unexpected formats. Fixes #2967 --- Shared/Extensions/IconImage.swift | 88 +++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 21 deletions(-) diff --git a/Shared/Extensions/IconImage.swift b/Shared/Extensions/IconImage.swift index dd11e0933..e460e4b6b 100644 --- a/Shared/Extensions/IconImage.swift +++ b/Shared/Extensions/IconImage.swift @@ -63,47 +63,81 @@ final class IconImage { fileprivate enum ImageLuminanceType { case regular, bright, dark } + extension CGImage { func isBright() -> Bool { - guard let imageData = self.dataProvider?.data, let luminanceType = getLuminanceType(from: imageData) else { + guard let luminanceType = getLuminanceType() else { return false } return luminanceType == .bright } func isDark() -> Bool { - guard let imageData = self.dataProvider?.data, let luminanceType = getLuminanceType(from: imageData) else { + guard let luminanceType = getLuminanceType() else { return false } return luminanceType == .dark } - fileprivate func getLuminanceType(from data: CFData) -> ImageLuminanceType? { - guard let ptr = CFDataGetBytePtr(data) else { - return nil - } - - let length = CFDataGetLength(data) - var pixelCount = 0 + fileprivate func getLuminanceType() -> ImageLuminanceType? { + + // This has been rewritten with information from https://christianselig.com/2021/04/efficient-average-color/ + + // First, resize the image. We do this for two reasons, 1) less pixels to deal with means faster + // calculation and a resized image still has the "gist" of the colors, and 2) the image we're dealing + // with may come in any of a variety of color formats (CMYK, ARGB, RGBA, etc.) which complicates things, + // and redrawing it normalizes that into a base color format we can deal with. + // 40x40 is a good size to resize to still preserve quite a bit of detail but not have too many pixels + // to deal with. Aspect ratio is irrelevant for just finding average color. + let size = CGSize(width: 40, height: 40) + + let width = Int(size.width) + let height = Int(size.height) + let totalPixels = width * height + + let colorSpace = CGColorSpaceCreateDeviceRGB() + + // ARGB format + let bitmapInfo: UInt32 = CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue + + // 8 bits for each color channel, we're doing ARGB so 32 bits (4 bytes) total, and thus if the image is n pixels wide, + // and has 4 bytes per pixel, the total bytes per row is 4n. That gives us 2^8 = 256 color variations for each RGB channel + // or 256 * 256 * 256 = ~16.7M color options in total. That seems like a lot, but lots of HDR movies are in 10 bit, which + // is (2^10)^3 = 1 billion color options! + guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo) else { return nil } + + // Draw our resized image + context.draw(self, in: CGRect(origin: .zero, size: size)) + + guard let pixelBuffer = context.data else { return nil } + + // Bind the pixel buffer's memory location to a pointer we can use/access + let pointer = pixelBuffer.bindMemory(to: UInt32.self, capacity: width * height) + var totalLuminance = 0.0 - for i in stride(from: 0, to: length, by: 4) { - - let r = ptr[i] - let g = ptr[i + 1] - let b = ptr[i + 2] - let a = ptr[i + 3] - let luminance = (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b)) - - if Double(a) > 0 { + // Column of pixels in image + for x in 0 ..< width { + // Row of pixels in image + for y in 0 ..< height { + // To get the pixel location just think of the image as a grid of pixels, but stored as one long row + // rather than columns and rows, so for instance to map the pixel from the grid in the 15th row and 3 + // columns in to our "long row", we'd offset ourselves 15 times the width in pixels of the image, and + // then offset by the amount of columns + let pixel = pointer[(y * width) + x] + + let r = red(for: pixel) + let g = green(for: pixel) + let b = blue(for: pixel) + + let luminance = (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b)) + totalLuminance += luminance - pixelCount += 1 } - } - let avgLuminance = totalLuminance / Double(pixelCount) + let avgLuminance = totalLuminance / Double(totalPixels) if totalLuminance == 0 || avgLuminance < 40 { return .dark } else if avgLuminance > 180 { @@ -113,6 +147,18 @@ extension CGImage { } } + private func red(for pixelData: UInt32) -> UInt8 { + return UInt8((pixelData >> 16) & 255) + } + + private func green(for pixelData: UInt32) -> UInt8 { + return UInt8((pixelData >> 8) & 255) + } + + private func blue(for pixelData: UInt32) -> UInt8 { + return UInt8((pixelData >> 0) & 255) + } + } From b69f936cb1d8621d9629f5344d02ef86e47f83ce Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 3 Apr 2021 11:02:15 -0500 Subject: [PATCH 35/35] Change the Mark All As Read confirmation back to an Alert. Fixes #2968 --- iOS/Base.lproj/Main.storyboard | 5 +++- .../MasterTimelineViewController.swift | 29 +------------------ 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index 7a89c6297..82dc391e2 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -18,7 +18,7 @@ -