diff --git a/Account/Package.swift b/Account/Package.swift index 3d49c12e0..040712530 100644 --- a/Account/Package.swift +++ b/Account/Package.swift @@ -13,7 +13,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.0")), + .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), .package(url: "../Articles", .upToNextMajor(from: "1.0.0")), .package(url: "../ArticlesDatabase", .upToNextMajor(from: "1.0.0")), diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift index fa58df337..e5f971c55 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZone.swift @@ -180,7 +180,11 @@ final class CloudKitAccountZone: CloudKitZone { } case .failure(let error): - completion(.failure(error)) + if let ckError = ((error as? CloudKitError)?.error as? CKError), ckError.code == .unknownItem { + completion(.success(true)) + } else { + completion(.failure(error)) + } } } } diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift index 73d347ec0..bc1224836 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountZoneDelegate.swift @@ -16,8 +16,9 @@ import Articles class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { private typealias UnclaimedWebFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String) - private var unclaimedWebFeeds = [String: [UnclaimedWebFeed]]() - + private var newUnclaimedWebFeeds = [String: [UnclaimedWebFeed]]() + private var existingUnclaimedWebFeeds = [String: [WebFeed]]() + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit") weak var account: Account? @@ -75,7 +76,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { if let container = account.existingContainer(withExternalID: containerExternalID) { createWebFeedIfNecessary(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, container: container) } else { - addUnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, containerExternalID: containerExternalID) + addNewUnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: record.externalID, containerExternalID: containerExternalID) } } } @@ -106,19 +107,27 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate { folder?.externalID = record.externalID } - if let folder = folder, let containerExternalID = folder.externalID, let unclaimedWebFeeds = unclaimedWebFeeds[containerExternalID] { - for unclaimedWebFeed in unclaimedWebFeeds { - createWebFeedIfNecessary(url: unclaimedWebFeed.url, - name: unclaimedWebFeed.name, - editedName: unclaimedWebFeed.editedName, - homePageURL: unclaimedWebFeed.homePageURL, - webFeedExternalID: unclaimedWebFeed.webFeedExternalID, - container: folder) + guard let container = folder, let containerExternalID = container.externalID else { return } + + if let newUnclaimedWebFeeds = newUnclaimedWebFeeds[containerExternalID] { + for newUnclaimedWebFeed in newUnclaimedWebFeeds { + createWebFeedIfNecessary(url: newUnclaimedWebFeed.url, + name: newUnclaimedWebFeed.name, + editedName: newUnclaimedWebFeed.editedName, + homePageURL: newUnclaimedWebFeed.homePageURL, + webFeedExternalID: newUnclaimedWebFeed.webFeedExternalID, + container: container) } - self.unclaimedWebFeeds.removeValue(forKey: containerExternalID) + self.newUnclaimedWebFeeds.removeValue(forKey: containerExternalID) } + if let existingUnclaimedWebFeeds = existingUnclaimedWebFeeds[containerExternalID] { + for existingUnclaimedWebFeed in existingUnclaimedWebFeeds { + container.addWebFeed(existingUnclaimedWebFeed) + } + self.existingUnclaimedWebFeeds.removeValue(forKey: containerExternalID) + } } func removeContainer(_ externalID: String) { @@ -152,6 +161,8 @@ private extension CloudKitAcountZoneDelegate { case .insert(_, let externalID, _): if let container = account.existingContainer(withExternalID: externalID) { container.addWebFeed(webFeed) + } else { + addExistingUnclaimedWebFeed(webFeed, containerExternalID: externalID) } } } @@ -170,14 +181,25 @@ private extension CloudKitAcountZoneDelegate { container.addWebFeed(webFeed) } - func addUnclaimedWebFeed(url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String, containerExternalID: String) { - if var unclaimedWebFeeds = self.unclaimedWebFeeds[containerExternalID] { + func addNewUnclaimedWebFeed(url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String, containerExternalID: String) { + if var unclaimedWebFeeds = self.newUnclaimedWebFeeds[containerExternalID] { unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID)) - self.unclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds + self.newUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds } else { var unclaimedWebFeeds = [UnclaimedWebFeed]() unclaimedWebFeeds.append(UnclaimedWebFeed(url: url, name: name, editedName: editedName, homePageURL: homePageURL, webFeedExternalID: webFeedExternalID)) - self.unclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds + self.newUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds + } + } + + func addExistingUnclaimedWebFeed(_ webFeed: WebFeed, containerExternalID: String) { + if var unclaimedWebFeeds = self.existingUnclaimedWebFeeds[containerExternalID] { + unclaimedWebFeeds.append(webFeed) + self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds + } else { + var unclaimedWebFeeds = [WebFeed]() + unclaimedWebFeeds.append(webFeed) + self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds } } diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterStatus.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterStatus.swift index 59f3dfc58..149814667 100644 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterStatus.swift +++ b/Account/Sources/Account/FeedProvider/Twitter/TwitterStatus.swift @@ -78,21 +78,21 @@ private extension TwitterStatus { var html = String() var prevIndex = displayStartIndex - var emojiOffset = 0 + var unicodeScalarOffset = 0 for entity in entities { - // The twitter indices are messed up by emoji with more than one scalar, we are going to adjust for that here. - let emojiEndIndex = text.index(text.startIndex, offsetBy: entity.endIndex, limitedBy: text.endIndex) ?? text.endIndex - if prevIndex < emojiEndIndex { - let emojis = String(text[prevIndex...self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (httpResponse, _)): if httpResponse.statusCode == 200 { diff --git a/Account/Sources/Account/NewsBlur/NewsBlurAPICaller.swift b/Account/Sources/Account/NewsBlur/NewsBlurAPICaller.swift index 02935e1d9..58849c6cd 100644 --- a/Account/Sources/Account/NewsBlur/NewsBlurAPICaller.swift +++ b/Account/Sources/Account/NewsBlur/NewsBlurAPICaller.swift @@ -133,7 +133,7 @@ final class NewsBlurAPICaller: NSObject { URLQueryItem(name: "page", value: String(page)), URLQueryItem(name: "order", value: "newest"), URLQueryItem(name: "read_filter", value: "all"), - URLQueryItem(name: "include_hidden", value: "true"), + URLQueryItem(name: "include_hidden", value: "false"), URLQueryItem(name: "include_story_content", value: "true"), ]) @@ -150,7 +150,7 @@ final class NewsBlurAPICaller: NSObject { func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) { let url = baseURL .appendingPathComponent("reader/river_stories") - .appendingQueryItem(.init(name: "include_hidden", value: "true"))? + .appendingQueryItem(.init(name: "include_hidden", value: "false"))? .appendingQueryItems(hashes.map { URLQueryItem(name: "h", value: $0.hash) }) diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index 84d8b7353..16d333340 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -14,10 +14,24 @@ import SyncDatabase import os.log import Secrets -public enum ReaderAPIAccountDelegateError: String, Error { - case unknown = "An unknown error occurred." - case invalidParameter = "There was an invalid parameter passed." - case invalidResponse = "There was an invalid response from the server." +public enum ReaderAPIAccountDelegateError: LocalizedError { + case unknown + case invalidParameter + case invalidResponse + case urlNotFound + + public var errorDescription: String? { + switch self { + case .unknown: + return NSLocalizedString("An unexpected error occurred.", comment: "An unexpected error occurred.") + case .invalidParameter: + return NSLocalizedString("An invalid parameter was passed.", comment: "An invalid parameter was passed.") + case .invalidResponse: + return NSLocalizedString("There was an invalid response from the server.", comment: "There was an invalid response from the server.") + case .urlNotFound: + return NSLocalizedString("The API URL wasn't found.", comment: "The API URL wasn't found.") + } + } } final class ReaderAPIAccountDelegate: AccountDelegate { diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift index 4e9f1b0c1..805fd3b8d 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift @@ -132,7 +132,11 @@ final class ReaderAPICaller: NSObject { completion(.success(self.credentials)) case .failure(let error): - completion(.failure(error)) + if let transportError = error as? TransportError, case .httpError(let code) = transportError, code == 404 { + completion(.failure(ReaderAPIAccountDelegateError.urlNotFound)) + } else { + completion(.failure(error)) + } } } diff --git a/ArticlesDatabase/Package.swift b/ArticlesDatabase/Package.swift index 6a9076bba..127979844 100644 --- a/ArticlesDatabase/Package.swift +++ b/ArticlesDatabase/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.0")), + .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), .package(url: "../Articles", .upToNextMajor(from: "1.0.0")), ], targets: [ diff --git a/Mac/Inspector/WebFeedInspectorViewController.swift b/Mac/Inspector/WebFeedInspectorViewController.swift index cd28699b8..58c24b254 100644 --- a/Mac/Inspector/WebFeedInspectorViewController.swift +++ b/Mac/Inspector/WebFeedInspectorViewController.swift @@ -149,18 +149,7 @@ private extension WebFeedInspectorViewController { guard let feed = feed, let iconView = iconView else { return } - - if let feedIcon = appDelegate.webFeedIconDownloader.icon(for: feed) { - iconView.iconImage = feedIcon - return - } - - if let favicon = appDelegate.faviconDownloader.favicon(for: feed) { - iconView.iconImage = favicon - return - } - - iconView.iconImage = feed.smallIcon + iconView.iconImage = IconImageCache.shared.imageForFeed(feed) } func updateName() { diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index 7184defd5..99b33cb90 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -768,7 +768,7 @@ private extension SidebarViewController { } func imageFor(_ node: Node) -> IconImage? { - if let feed = node.representedObject as? WebFeed, let feedIcon = appDelegate.webFeedIconDownloader.icon(for: feed) { + if let feed = node.representedObject as? WebFeed, let feedIcon = IconImageCache.shared.imageForFeed(feed) { return feedIcon } if let smallIconProvider = node.representedObject as? SmallIconProvider { diff --git a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift index 272c55e16..908eed74b 100644 --- a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift +++ b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift @@ -38,7 +38,7 @@ extension Article: PasteboardWriterOwner { func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] { var types = [ArticlePasteboardWriter.articleUTIType] - if let link = article.preferredLink, let _ = URL(string: link) { + if let _ = article.preferredURL { types += [.URL] } types += [.string, .html, ArticlePasteboardWriter.articleUTIInternalType] diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index fff7b1324..38cec256e 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -886,28 +886,7 @@ extension TimelineViewController: NSTableViewDelegate { if !showIcons { return nil } - - if let authors = article.authors { - for author in authors { - if let image = avatarForAuthor(author) { - return image - } - } - } - - guard let feed = article.webFeed else { - return nil - } - - if let feedIcon = appDelegate.webFeedIconDownloader.icon(for: feed) { - return feedIcon - } - - if let favicon = appDelegate.faviconDownloader.faviconAsIcon(for: feed) { - return favicon - } - - return FaviconGenerator.favicon(feed) + return IconImageCache.shared.imageForArticle(article) } private func avatarForAuthor(_ author: Author) -> IconImage? { diff --git a/Multiplatform/Shared/Article/ArticleToolbarModifier.swift b/Multiplatform/Shared/Article/ArticleToolbarModifier.swift index dd148e4aa..380cba519 100644 --- a/Multiplatform/Shared/Article/ArticleToolbarModifier.swift +++ b/Multiplatform/Shared/Article/ArticleToolbarModifier.swift @@ -107,7 +107,7 @@ struct ArticleToolbarModifier: ViewModifier { .disabled(sceneModel.shareButtonState == nil) .help("Share") .sheet(isPresented: $showActivityView) { - if let article = sceneModel.selectedArticles.first, let link = article.preferredLink, let url = URL(string: link) { + if let article = sceneModel.selectedArticles.first, let url = article.preferredURL { ActivityViewController(title: article.title, url: url) } } diff --git a/Multiplatform/Shared/Images/FeedIconImageLoader.swift b/Multiplatform/Shared/Images/FeedIconImageLoader.swift index 9ee4d4bef..d4c041bd5 100644 --- a/Multiplatform/Shared/Images/FeedIconImageLoader.swift +++ b/Multiplatform/Shared/Images/FeedIconImageLoader.swift @@ -41,20 +41,6 @@ final class FeedIconImageLoader: ObservableObject { private extension FeedIconImageLoader { func fetchImage() { - if let webFeed = feed as? WebFeed { - if let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed) { - image = feedIconImage - return - } - if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) { - image = faviconImage - return - } - } - - if let smallIconProvider = feed as? SmallIconProvider { - image = smallIconProvider.smallIcon - } + image = IconImageCache.shared.imageForFeed(feed) } - } diff --git a/Multiplatform/Shared/Timeline/TimelineModel.swift b/Multiplatform/Shared/Timeline/TimelineModel.swift index a70cd6c69..35b1ed020 100644 --- a/Multiplatform/Shared/Timeline/TimelineModel.swift +++ b/Multiplatform/Shared/Timeline/TimelineModel.swift @@ -249,12 +249,11 @@ class TimelineModel: ObservableObject, UndoableCommandRunner { } func openIndicatedArticleInBrowser(_ article: Article) { - guard let link = article.preferredLink else { return } - #if os(macOS) + guard let link = article.preferredLink else { return } Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false) #else - guard let url = URL(string: link) else { return } + guard let url = article.preferredURL else { return } UIApplication.shared.open(url, options: [:]) #endif } diff --git a/Multiplatform/iOS/Settings/About/Dedication.rtf b/Multiplatform/iOS/Settings/About/Dedication.rtf index 328808bb7..974f1b818 100644 --- a/Multiplatform/iOS/Settings/About/Dedication.rtf +++ b/Multiplatform/iOS/Settings/About/Dedication.rtf @@ -1,9 +1,9 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2511 +{\rtf1\ansi\ansicpg1252\cocoartf2513 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;} -{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;} +{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;} \margl1440\margr1440\vieww9000\viewh8400\viewkind0 \deftab720 -\pard\pardeftab720\li354\fi-355\sa60\partightenfactor0 +\pard\pardeftab720\sa60\partightenfactor0 -\f0\fs28 \cf2 NetNewsWire 5.0 is dedicated to all the people who showed up to help with code, design, HTML, documentation, icons, testing, and just to help talk things over and think things through. This app\'92s for you!} \ No newline at end of file +\f0\fs22 \cf2 NetNewsWire 6 is dedicated to everyone working to save democracy around the world.} \ No newline at end of file diff --git a/Multiplatform/iOS/Settings/About/SettingsAboutView.swift b/Multiplatform/iOS/Settings/About/SettingsAboutView.swift index a6e0938fa..e4a02adb4 100644 --- a/Multiplatform/iOS/Settings/About/SettingsAboutView.swift +++ b/Multiplatform/iOS/Settings/About/SettingsAboutView.swift @@ -24,7 +24,7 @@ struct SettingsAboutView: View { Section(header: Text("THANKS")) { AttributedStringView(string: self.viewModel.thanks, preferredMaxLayoutWidth: geometry.size.width - 20) } - Section(header: Text("DEDICATION"), footer: Text("Copyright © 2002-2020 BrentSimmons").font(.footnote)) { + Section(header: Text("DEDICATION"), footer: Text("Copyright © 2002-2021 Brent Simmons").font(.footnote)) { AttributedStringView(string: self.viewModel.dedication, preferredMaxLayoutWidth: geometry.size.width - 20) } }.listStyle(InsetGroupedListStyle()) diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 04c5227ff..6563224de 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -1015,6 +1015,9 @@ 844B5B691FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */; }; 845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; }; 845479881FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */; }; + 8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */; }; + 8454C3F8263F3AD400E3F9C7 /* IconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */; }; + 8454C3FD263F3AD600E3F9C7 /* IconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */; }; 845A29091FC74B8E007B49E3 /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; }; 845A29221FC9251E007B49E3 /* SidebarCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */; }; 845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29231FC9255E007B49E3 /* SidebarCellAppearance.swift */; }; @@ -1901,6 +1904,7 @@ 844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = SidebarKeyboardShortcuts.plist; sourceTree = ""; }; 845213221FCA5B10003B6E93 /* ImageDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; }; 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = TimelineKeyboardShortcuts.plist; sourceTree = ""; }; + 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImageCache.swift; sourceTree = ""; }; 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFaviconDownloader.swift; sourceTree = ""; }; 845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarCellLayout.swift; sourceTree = ""; }; 845A29231FC9255E007B49E3 /* SidebarCellAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarCellAppearance.swift; sourceTree = ""; }; @@ -3436,6 +3440,7 @@ 842E45CD1ED8C308000A8B52 /* AppNotifications.swift */, 51C4CFEF24D37D1F00AF9874 /* Secrets.swift */, 511B9805237DCAC90028BCAA /* UserInfoKey.swift */, + 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */, 51C452AD2265102800C03939 /* Timeline */, 84702AB31FA27AE8006B8943 /* Commands */, 51934CCC231078DC006127BE /* Activity */, @@ -5150,6 +5155,7 @@ 65ED4007235DEF6C0081F399 /* AddFeedController.swift in Sources */, 65ED4008235DEF6C0081F399 /* AccountRefreshTimer.swift in Sources */, 65ED4009235DEF6C0081F399 /* SidebarStatusBarView.swift in Sources */, + 8454C3FD263F3AD600E3F9C7 /* IconImageCache.swift in Sources */, 65ED400A235DEF6C0081F399 /* SearchTimelineFeedDelegate.swift in Sources */, 65ED400B235DEF6C0081F399 /* TodayFeedDelegate.swift in Sources */, 65ED400C235DEF6C0081F399 /* FolderInspectorViewController.swift in Sources */, @@ -5375,6 +5381,7 @@ 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */, 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */, D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */, + 8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */, B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */, C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */, 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */, @@ -5545,6 +5552,7 @@ 849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */, 841ABA6020145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift in Sources */, D5E4CC54202C1361009B4FFC /* AppDelegate+Scriptability.swift in Sources */, + 8454C3F8263F3AD400E3F9C7 /* IconImageCache.swift in Sources */, 518651B223555EB20078E021 /* NNW3Document.swift in Sources */, D5F4EDB5200744A700B9E363 /* ScriptingObject.swift in Sources */, D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */, diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index 300aaa255..266911e5d 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -248,12 +248,11 @@ private extension ActivityManager { attributeSet.title = feed.nameForDisplay attributeSet.keywords = makeKeywords(feed.nameForDisplay) attributeSet.relatedUniqueIdentifier = ActivityManager.identifer(for: feed) - if let iconImage = appDelegate.webFeedIconDownloader.icon(for: feed) { - attributeSet.thumbnailData = iconImage.image.dataRepresentation() - } else if let iconImage = appDelegate.faviconDownloader.faviconAsIcon(for: feed) { + + if let iconImage = IconImageCache.shared.imageForFeed(feed) { attributeSet.thumbnailData = iconImage.image.dataRepresentation() } - + selectingActivity!.contentAttributeSet = attributeSet selectingActivity!.needsSave = true diff --git a/Shared/Article Rendering/newsfoot.js b/Shared/Article Rendering/newsfoot.js index 1a53dbe42..aba1bc8ec 100644 --- a/Shared/Article Rendering/newsfoot.js +++ b/Shared/Article Rendering/newsfoot.js @@ -157,7 +157,7 @@ document.addEventListener("click", (ev) => { if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return; - if (!ev.target.matches(".footnotes .reversefootnote, .footnotes .footnoteBackLink, .footnotes .footnote-return")) return; + if (!ev.target.matches(".footnotes .reversefootnote, .footnotes .footnoteBackLink, .footnotes .footnote-return, .footnotes a[href*='#fn'], .footnotes a[href^='#']")) return; const id = idFromHash(ev.target); if (!id) return; const fnref = document.getElementById(id); diff --git a/Shared/Article Rendering/shared.css b/Shared/Article Rendering/shared.css index 29707dd0b..54449410f 100644 --- a/Shared/Article Rendering/shared.css +++ b/Shared/Article Rendering/shared.css @@ -382,7 +382,8 @@ img[src*="share-buttons"] { .newsfoot-footnote-popover .reversefootnote, .newsfoot-footnote-popover .footnoteBackLink, -.newsfoot-footnote-popover .footnote-return { +.newsfoot-footnote-popover .footnote-return, +.newsfoot-footnote-popover a[href*='#fn'] { display: none; } diff --git a/Shared/Commands/MarkStatusCommand.swift b/Shared/Commands/MarkStatusCommand.swift index e6eae4b54..79cf9777c 100644 --- a/Shared/Commands/MarkStatusCommand.swift +++ b/Shared/Commands/MarkStatusCommand.swift @@ -27,6 +27,7 @@ final class MarkStatusCommand: UndoableCommand { // Filter out articles that already have the desired status or can't be marked. let articlesToMark = MarkStatusCommand.filteredArticles(initialArticles, statusKey, flag) if articlesToMark.isEmpty { + completion?() return nil } self.articles = Set(articlesToMark) diff --git a/Shared/Extensions/ArticleUtilities.swift b/Shared/Extensions/ArticleUtilities.swift index 5e1822cba..ace388e04 100644 --- a/Shared/Extensions/ArticleUtilities.swift +++ b/Shared/Extensions/ArticleUtilities.swift @@ -56,6 +56,18 @@ extension Article { return nil } + var preferredURL: URL? { + guard let link = preferredLink else { return nil } + // If required, we replace any space characters to handle malformed links that are otherwise percent + // encoded but contain spaces. For performance reasons, only try this if initial URL init fails. + if let url = URL(string: link) { + return url + } else if let url = URL(string: link.replacingOccurrences(of: " ", with: "%20")) { + return url + } + return nil + } + var body: String? { return contentHTML ?? contentText ?? summary } @@ -84,32 +96,7 @@ extension Article { } func iconImage() -> IconImage? { - if let authors = authors, authors.count == 1, let author = authors.first { - if let image = appDelegate.authorAvatarDownloader.image(for: author) { - return image - } - } - - if let authors = webFeed?.authors, authors.count == 1, let author = authors.first { - if let image = appDelegate.authorAvatarDownloader.image(for: author) { - return image - } - } - - guard let webFeed = webFeed else { - return nil - } - - let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed) - if feedIconImage != nil { - return feedIconImage - } - - if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) { - return faviconImage - } - - return FaviconGenerator.favicon(webFeed) + return IconImageCache.shared.imageForArticle(self) } func iconImageUrl(webFeed: WebFeed) -> URL? { diff --git a/Shared/IconImageCache.swift b/Shared/IconImageCache.swift new file mode 100644 index 000000000..1a7bc1826 --- /dev/null +++ b/Shared/IconImageCache.swift @@ -0,0 +1,129 @@ +// +// IconImageCache.swift +// NetNewsWire-iOS +// +// Created by Brent Simmons on 5/2/21. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation +import Account +import Articles + +class IconImageCache { + + static var shared = IconImageCache() + + private var smartFeedIconImageCache = [FeedIdentifier: IconImage]() + private var webFeedIconImageCache = [FeedIdentifier: IconImage]() + private var faviconImageCache = [FeedIdentifier: IconImage]() + private var smallIconImageCache = [FeedIdentifier: IconImage]() + private var authorIconImageCache = [Author: IconImage]() + + func imageFor(_ feedID: FeedIdentifier) -> IconImage? { + if let smartFeed = SmartFeedsController.shared.find(by: feedID) { + return imageForFeed(smartFeed) + } + if let feed = AccountManager.shared.existingFeed(with: feedID) { + return imageForFeed(feed) + } + return nil + } + + func imageForFeed(_ feed: Feed) -> IconImage? { + guard let feedID = feed.feedID else { + return nil + } + + if let smartFeed = feed as? PseudoFeed { + return imageForSmartFeed(smartFeed, feedID) + } + if let webFeed = feed as? WebFeed, let iconImage = imageForWebFeed(webFeed, feedID) { + return iconImage + } + if let smallIconProvider = feed as? SmallIconProvider { + return imageForSmallIconProvider(smallIconProvider, feedID) + } + + return nil + } + + func imageForArticle(_ article: Article) -> IconImage? { + if let iconImage = imageForAuthors(article.authors) { + return iconImage + } + guard let feed = article.webFeed else { + return nil + } + return imageForFeed(feed) + } + + func emptyCache() { + smartFeedIconImageCache = [FeedIdentifier: IconImage]() + webFeedIconImageCache = [FeedIdentifier: IconImage]() + faviconImageCache = [FeedIdentifier: IconImage]() + smallIconImageCache = [FeedIdentifier: IconImage]() + authorIconImageCache = [Author: IconImage]() + } +} + +private extension IconImageCache { + + func imageForSmartFeed(_ smartFeed: PseudoFeed, _ feedID: FeedIdentifier) -> IconImage? { + if let iconImage = smartFeedIconImageCache[feedID] { + return iconImage + } + if let iconImage = smartFeed.smallIcon { + smartFeedIconImageCache[feedID] = iconImage + return iconImage + } + return nil + } + + func imageForWebFeed(_ webFeed: WebFeed, _ feedID: FeedIdentifier) -> IconImage? { + if let iconImage = webFeedIconImageCache[feedID] { + return iconImage + } + if let iconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed) { + webFeedIconImageCache[feedID] = iconImage + return iconImage + } + if let faviconImage = faviconImageCache[feedID] { + return faviconImage + } + if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) { + faviconImageCache[feedID] = faviconImage + return faviconImage + } + return nil + } + + func imageForSmallIconProvider(_ provider: SmallIconProvider, _ feedID: FeedIdentifier) -> IconImage? { + if let iconImage = smallIconImageCache[feedID] { + return iconImage + } + if let iconImage = provider.smallIcon { + smallIconImageCache[feedID] = iconImage + return iconImage + } + return nil + } + + func imageForAuthors(_ authors: Set?) -> IconImage? { + guard let authors = authors, authors.count == 1, let author = authors.first else { + return nil + } + return imageForAuthor(author) + } + + func imageForAuthor(_ author: Author) -> IconImage? { + if let iconImage = authorIconImageCache[author] { + return iconImage + } + if let iconImage = appDelegate.authorAvatarDownloader.image(for: author) { + authorIconImageCache[author] = iconImage + return iconImage + } + return nil + } +} diff --git a/Shared/Tree/WebFeedTreeControllerDelegate.swift b/Shared/Tree/WebFeedTreeControllerDelegate.swift index cd1da83ef..60eceef26 100644 --- a/Shared/Tree/WebFeedTreeControllerDelegate.swift +++ b/Shared/Tree/WebFeedTreeControllerDelegate.swift @@ -56,9 +56,7 @@ private extension WebFeedTreeControllerDelegate { func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] { return SmartFeedsController.shared.smartFeeds.compactMap { (feed) -> Node? in - if let feedID = feed.feedID, !filterExceptions.contains(feedID) && isReadFiltered && feed.unreadCount == 0 { - return nil - } + // All Smart Feeds should remain visible despite the Hide Read Feeds setting return parentNode.existingOrNewChildNode(with: feed as AnyObject) } } diff --git a/Technotes/ReleaseNotes-iOS.markdown b/Technotes/ReleaseNotes-iOS.markdown new file mode 100644 index 000000000..0054166eb --- /dev/null +++ b/Technotes/ReleaseNotes-iOS.markdown @@ -0,0 +1,25 @@ +# iOS Release Notes + +### 6.0 TestFlight build 603 - 16 May 2021 + +Feedly: handle Feedly API change with return value on deleting a folder +NewsBlur: sync no longer includes items marked as hidden on NewsBlur +FreshRSS: form for adding account now suggests endpoing URL +FreshRSS: improved the error message for when the API URL can’t be found +iCloud: retain existing feeds moved to a folder that doesn’t exist yet (sync ordering issue) +Renamed a Delete Account button to Remove Account +iCloud: skip displaying an error message on deleting a feed that doesn’t exist in iCloud +Preferences: Tweaked text explaining Feed Providers +Feeds list: context menu for smart feeds is back (regression fix) +Feeds list: all smart feeds remain visible despite Hide Read Feeds setting +Article view: fixed zoom issue on iPad on rotation +Article view: fixed bug where mark-read button on toolbar would flash on navigating to an unread article +Article view: made footnote detection more robust +Fixed regression on iPad where timeline and article wouldn’t update after the selected feed was deleted +Sharing: handle feeds where the URL has unencoded space characters (why a feed would do that is beyond our ken) + +### 6.0 TestFlight build 602 - 21 April 2021 + +Inoreader: don’t call it so often, so we don’t go over the API limits +Feedly: handle a specific case where Feedly started not returning a value we expected but didn’t actually need (we were reporting it as an error to the user, but it wasn’t) + diff --git a/iOS/Account/ReaderAPIAccountViewController.swift b/iOS/Account/ReaderAPIAccountViewController.swift index 4e60f7c3b..16432a797 100644 --- a/iOS/Account/ReaderAPIAccountViewController.swift +++ b/iOS/Account/ReaderAPIAccountViewController.swift @@ -50,6 +50,7 @@ class ReaderAPIAccountViewController: UITableViewController { switch unwrappedAccountType { case .freshRSS: title = NSLocalizedString("FreshRSS", comment: "FreshRSS") + apiURLTextField.placeholder = NSLocalizedString("API URL: fresh.rss.net/api/greader.php", comment: "FreshRSS API Helper") case .inoreader: title = NSLocalizedString("InoReader", comment: "InoReader") case .bazQux: diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 6e1bab493..d4fa54768 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -132,6 +132,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func applicationWillTerminate(_ application: UIApplication) { shuttingDown = true } + + func applicationDidEnterBackground(_ application: UIApplication) { + IconImageCache.shared.emptyCache() + } // MARK: Notifications diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 74c8404c6..c5b776126 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -79,6 +79,13 @@ class WebViewController: UIViewController { } + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + // We need to reload the webview on the iPhone when rotation happens to clear out any old bad viewport sizes + if traitCollection.userInterfaceIdiom == .phone { + loadWebView() + } + } + // MARK: Notifications @objc func webFeedIconDidBecomeAvailable(_ note: Notification) { @@ -235,20 +242,14 @@ class WebViewController: UIViewController { } func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) { - guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else { - return - } - + guard let url = article?.preferredURL else { return } let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()]) activityViewController.popoverPresentationController?.barButtonItem = popOverBarButtonItem present(activityViewController, animated: true) } func openInAppBrowser() { - guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else { - return - } - + guard let url = article?.preferredURL else { return } let vc = SFSafariViewController(url: url) present(vc, animated: true) } diff --git a/iOS/IconView.swift b/iOS/IconView.swift index 55cd454cc..90c61b150 100644 --- a/iOS/IconView.swift +++ b/iOS/IconView.swift @@ -12,33 +12,24 @@ final class IconView: UIView { var iconImage: IconImage? = nil { didSet { - if iconImage !== oldValue { - imageView.image = iconImage?.image - - if self.traitCollection.userInterfaceStyle == .dark { - if self.iconImage?.isDark ?? false { - self.isDiscernable = false - self.setNeedsLayout() - } else { - self.isDiscernable = true - self.setNeedsLayout() - } - } else { - if self.iconImage?.isBright ?? false { - self.isDiscernable = false - self.setNeedsLayout() - } else { - self.isDiscernable = true - self.setNeedsLayout() - } - } - self.setNeedsLayout() + guard iconImage !== oldValue else { + return } + imageView.image = iconImage?.image + if traitCollection.userInterfaceStyle == .dark { + let isDark = iconImage?.isDark ?? false + isDiscernable = !isDark + } + else { + let isBright = iconImage?.isBright ?? false + isDiscernable = !isBright + } + setNeedsLayout() } } private var isDiscernable = true - + private let imageView: UIImageView = { let imageView = NonIntrinsicImageView(image: AppAssets.faviconTemplateImage) imageView.contentMode = .scaleAspectFit @@ -79,13 +70,8 @@ final class IconView: UIView { override func layoutSubviews() { imageView.setFrameIfNotEqual(rectForImageView()) - if !isBackgroundSuppressed && ((iconImage != nil && isVerticalBackgroundExposed) || !isDiscernable) { - backgroundColor = AppAssets.iconBackgroundColor - } else { - backgroundColor = nil - } + updateBackgroundColor() } - } private extension IconView { @@ -125,4 +111,11 @@ private extension IconView { return CGRect(x: 0.0, y: originY, width: viewSize.width, height: height) } + private func updateBackgroundColor() { + if !isBackgroundSuppressed && ((iconImage != nil && isVerticalBackgroundExposed) || !isDiscernable) { + backgroundColor = AppAssets.iconBackgroundColor + } else { + backgroundColor = nil + } + } } diff --git a/iOS/Inspector/AccountInspectorViewController.swift b/iOS/Inspector/AccountInspectorViewController.swift index 77a6082d6..f10257da6 100644 --- a/iOS/Inspector/AccountInspectorViewController.swift +++ b/iOS/Inspector/AccountInspectorViewController.swift @@ -92,13 +92,13 @@ class AccountInspectorViewController: UITableViewController { return } - let title = NSLocalizedString("Delete Account", comment: "Delete Account") + let title = NSLocalizedString("Remove Account", comment: "Remove Account") let message: String = { switch account.type { case .feedly: - return NSLocalizedString("Are you sure you want to delete this account? NetNewsWire will no longer be able to access articles and feeds unless the account is added again.", comment: "Log Out and Delete Account") + return NSLocalizedString("Are you sure you want to remove this account? NetNewsWire will no longer be able to access articles and feeds unless the account is added again.", comment: "Log Out and Remove Account") default: - return NSLocalizedString("Are you sure you want to delete this account? This cannot be undone.", comment: "Delete Account") + return NSLocalizedString("Are you sure you want to remove this account? This cannot be undone.", comment: "Remove Account") } }() let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) @@ -106,7 +106,7 @@ class AccountInspectorViewController: UITableViewController { let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) alertController.addAction(cancelAction) - let markTitle = NSLocalizedString("Delete", comment: "Delete") + let markTitle = NSLocalizedString("Remove", comment: "Remove") let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in guard let self = self, let account = self.account else { return } AccountManager.shared.deleteAccount(account) diff --git a/iOS/Inspector/Inspector.storyboard b/iOS/Inspector/Inspector.storyboard index 384157ff9..45fcc87f2 100644 --- a/iOS/Inspector/Inspector.storyboard +++ b/iOS/Inspector/Inspector.storyboard @@ -1,9 +1,9 @@ - + - + @@ -27,10 +27,10 @@ - + - + @@ -52,7 +52,7 @@