Merge branch 'master' into feature/feed-wrangler

# Conflicts:
#	Frameworks/Account/Account.swift
#	Frameworks/Account/Account.xcodeproj/project.pbxproj
#	NetNewsWire.xcodeproj/project.pbxproj
#	submodules/RSCore
This commit is contained in:
Jonathan Bennett
2019-11-20 14:01:04 -05:00
355 changed files with 89315 additions and 5653 deletions

View File

@@ -19,7 +19,7 @@ public enum ReaderAPIAccountDelegateError: String, Error {
}
final class ReaderAPIAccountDelegate: AccountDelegate {
private let database: SyncDatabase
private let caller: ReaderAPICaller
@@ -79,6 +79,10 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
var refreshProgress = DownloadProgress(numberOfTasks: 0)
func cancelAll(for account: Account) {
caller.cancelAll()
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(6)
@@ -87,8 +91,8 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
switch result {
case .success():
self.sendArticleStatus(for: account) {
self.refreshArticleStatus(for: account) {
self.sendArticleStatus(for: account) { _ in
self.refreshArticleStatus(for: account) { _ in
self.refreshArticles(account) {
self.refreshMissingArticles(account) {
self.refreshProgress.clear()
@@ -112,7 +116,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
}
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
os_log(.debug, log: log, "Sending article statuses...")
@@ -146,12 +150,12 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done sending article statuses.")
completion()
completion(.success(()))
}
}
func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
os_log(.debug, log: log, "Refreshing article statuses...")
@@ -185,7 +189,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
group.notify(queue: DispatchQueue.main) {
os_log(.debug, log: self.log, "Done refreshing article statuses.")
completion()
completion(.success(()))
}
}
@@ -223,9 +227,9 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
let group = DispatchGroup()
for feed in folder.topLevelFeeds {
for feed in folder.topLevelWebFeeds {
group.enter()
removeFeed(for: account, with: feed, from: folder) { result in
removeWebFeed(for: account, with: feed, from: folder) { result in
group.leave()
switch result {
case .success:
@@ -252,7 +256,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
}
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
caller.createSubscription(url: url) { result in
switch result {
@@ -280,7 +284,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
}
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
// This error should never happen
guard let subscriptionID = feed.subscriptionID else {
@@ -305,23 +309,23 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
}
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if feed.folderRelationship?.count ?? 0 > 1 {
deleteTagging(for: account, with: feed, from: container, completion: completion)
} else {
account.clearFeedMetadata(feed)
account.clearWebFeedMetadata(feed)
deleteSubscription(for: account, with: feed, from: container, completion: completion)
}
}
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if from is Account {
addFeed(for: account, with: feed, to: to, completion: completion)
addWebFeed(for: account, with: feed, to: to, completion: completion)
} else {
deleteTagging(for: account, with: feed, from: from) { result in
switch result {
case .success:
self.addFeed(for: account, with: feed, to: to, completion: completion)
self.addWebFeed(for: account, with: feed, to: to, completion: completion)
case .failure(let error):
completion(.failure(error))
}
@@ -329,7 +333,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
}
}
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if let folder = container as? Folder, let feedName = feed.subscriptionID {
caller.createTagging(subscriptionID: feedName, tagName: folder.name ?? "") { result in
@@ -337,8 +341,8 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
case .success:
DispatchQueue.main.async {
self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: feed.subscriptionID!)
account.removeFeed(feed)
folder.addFeed(feed)
account.removeWebFeed(feed)
folder.addWebFeed(feed)
completion(.success(()))
}
case .failure(let error):
@@ -359,9 +363,9 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
}
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in
createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in
switch result {
case .success:
completion(.success(()))
@@ -377,12 +381,12 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
account.addFolder(folder)
let group = DispatchGroup()
for feed in folder.topLevelFeeds {
for feed in folder.topLevelWebFeeds {
group.enter()
addFeed(for: account, with: feed, to: folder) { result in
if account.topLevelFeeds.contains(feed) {
account.removeFeed(feed)
addWebFeed(for: account, with: feed, to: folder) { result in
if account.topLevelWebFeeds.contains(feed) {
account.removeWebFeed(feed)
}
group.leave()
}
@@ -403,7 +407,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
database.insertStatuses(syncStatuses)
if database.selectPendingCount() > 100 {
sendArticleStatus(for: account) {}
sendArticleStatus(for: account) { _ in }
}
return account.update(articles, statusKey: statusKey, flag: flag)
@@ -414,6 +418,9 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
credentials = try? account.retrieveCredentials(type: .readerAPIKey)
}
func accountWillBeDeleted(_ account: Account) {
}
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void) {
guard let endpoint = endpoint else {
completion(.failure(TransportError.noURL))
@@ -465,8 +472,8 @@ private extension ReaderAPIAccountDelegate {
if let folders = account.folders {
folders.forEach { folder in
if !tagNames.contains(folder.name ?? "") {
for feed in folder.topLevelFeeds {
account.addFeed(feed)
for feed in folder.topLevelWebFeeds {
account.addWebFeed(feed)
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
}
account.removeFolder(folder)
@@ -528,17 +535,17 @@ private extension ReaderAPIAccountDelegate {
// Remove any feeds that are no longer in the subscriptions
if let folders = account.folders {
for folder in folders {
for feed in folder.topLevelFeeds {
if !subFeedIds.contains(feed.feedID) {
folder.removeFeed(feed)
for feed in folder.topLevelWebFeeds {
if !subFeedIds.contains(feed.webFeedID) {
folder.removeWebFeed(feed)
}
}
}
}
for feed in account.topLevelFeeds {
if !subFeedIds.contains(feed.feedID) {
account.removeFeed(feed)
for feed in account.topLevelWebFeeds {
if !subFeedIds.contains(feed.webFeedID) {
account.removeWebFeed(feed)
}
}
@@ -546,14 +553,14 @@ private extension ReaderAPIAccountDelegate {
subscriptions.forEach { subscription in
let subFeedId = String(subscription.feedID)
if let feed = account.existingFeed(withFeedID: subFeedId) {
if let feed = account.existingWebFeed(withWebFeedID: subFeedId) {
feed.name = subscription.name
feed.homePageURL = subscription.homePageURL
} else {
let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: subFeedId, homePageURL: subscription.homePageURL)
let feed = account.createWebFeed(with: subscription.name, url: subscription.url, webFeedID: subFeedId, homePageURL: subscription.homePageURL)
feed.iconURL = subscription.iconURL
feed.subscriptionID = String(subscription.feedID)
account.addFeed(feed)
account.addWebFeed(feed)
}
}
@@ -602,25 +609,25 @@ private extension ReaderAPIAccountDelegate {
let taggingFeedIDs = groupedTaggings.map { String($0.feedID) }
// Move any feeds not in the folder to the account
for feed in folder.topLevelFeeds {
if !taggingFeedIDs.contains(feed.feedID) {
folder.removeFeed(feed)
for feed in folder.topLevelWebFeeds {
if !taggingFeedIDs.contains(feed.webFeedID) {
folder.removeWebFeed(feed)
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
account.addFeed(feed)
account.addWebFeed(feed)
}
}
// Add any feeds not in the folder
let folderFeedIds = folder.topLevelFeeds.map { $0.feedID }
let folderFeedIds = folder.topLevelWebFeeds.map { $0.webFeedID }
for subscription in groupedTaggings {
let taggingFeedID = String(subscription.feedID)
if !folderFeedIds.contains(taggingFeedID) {
guard let feed = account.existingFeed(withFeedID: taggingFeedID) else {
guard let feed = account.existingWebFeed(withWebFeedID: taggingFeedID) else {
continue
}
saveFolderRelationship(for: feed, withFolderName: folderName, id: String(subscription.feedID))
folder.addFeed(feed)
folder.addWebFeed(feed)
}
}
@@ -629,9 +636,9 @@ private extension ReaderAPIAccountDelegate {
let taggedFeedIDs = Set(subscriptions.map { String($0.feedID) })
// Remove all feeds from the account container that have a tag
for feed in account.topLevelFeeds {
if taggedFeedIDs.contains(feed.feedID) {
account.removeFeed(feed)
for feed in account.topLevelWebFeeds {
if taggedFeedIDs.contains(feed.webFeedID) {
account.removeWebFeed(feed)
}
}
@@ -675,14 +682,14 @@ private extension ReaderAPIAccountDelegate {
func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) {
func clearFolderRelationship(for feed: WebFeed, withFolderName folderName: String) {
if var folderRelationship = feed.folderRelationship {
folderRelationship[folderName] = nil
feed.folderRelationship = folderRelationship
}
}
func saveFolderRelationship(for feed: Feed, withFolderName folderName: String, id: String) {
func saveFolderRelationship(for feed: WebFeed, withFolderName folderName: String, id: String) {
if var folderRelationship = feed.folderRelationship {
folderRelationship[folderName] = id
feed.folderRelationship = folderRelationship
@@ -691,7 +698,7 @@ private extension ReaderAPIAccountDelegate {
}
}
func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [ReaderAPISubscriptionChoice], completion: @escaping (Result<Feed, Error>) -> Void) {
func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [ReaderAPISubscriptionChoice], completion: @escaping (Result<WebFeed, Error>) -> Void) {
let feedSpecifiers: [FeedSpecifier] = choices.map { choice in
let source = url == choice.url ? FeedSpecifier.Source.UserEntered : FeedSpecifier.Source.HTMLLink
@@ -701,7 +708,7 @@ private extension ReaderAPIAccountDelegate {
if let bestSpecifier = FeedSpecifier.bestFeed(in: Set(feedSpecifiers)) {
if let bestSubscription = choices.filter({ bestSpecifier.urlString == $0.url }).first {
createFeed(for: account, url: bestSubscription.url, name: name, container: container, completion: completion)
createWebFeed(for: account, url: bestSubscription.url, name: name, container: container, completion: completion)
} else {
DispatchQueue.main.async {
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
@@ -715,18 +722,18 @@ private extension ReaderAPIAccountDelegate {
}
func createFeed( account: Account, subscription sub: ReaderAPISubscription, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
func createFeed( account: Account, subscription sub: ReaderAPISubscription, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
DispatchQueue.main.async {
let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL)
let feed = account.createWebFeed(with: sub.name, url: sub.url, webFeedID: String(sub.feedID), homePageURL: sub.homePageURL)
feed.subscriptionID = String(sub.feedID)
account.addFeed(feed, to: container) { result in
account.addWebFeed(feed, to: container) { result in
switch result {
case .success:
if let name = name {
account.renameFeed(feed, to: name) { result in
account.renameWebFeed(feed, to: name) { result in
switch result {
case .success:
self.initialFeedDownload(account: account, feed: feed, completion: completion)
@@ -746,16 +753,16 @@ private extension ReaderAPIAccountDelegate {
}
func initialFeedDownload( account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
func initialFeedDownload( account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) {
// Download the initial articles
self.caller.retrieveEntries(feedID: feed.feedID) { result in
self.caller.retrieveEntries(webFeedID: feed.webFeedID) { result in
switch result {
case .success(let (entries, page)):
self.processEntries(account: account, entries: entries) {
self.refreshArticleStatus(for: account) {
self.refreshArticleStatus(for: account) { _ in
self.refreshArticles(account, page: page) {
self.refreshMissingArticles(account) {
DispatchQueue.main.async {
@@ -867,8 +874,8 @@ private extension ReaderAPIAccountDelegate {
func processEntries(account: Account, entries: [ReaderAPIEntry]?, completion: @escaping (() -> Void)) {
let parsedItems = mapEntriesToParsedItems(account: account, entries: entries)
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true, completion: completion)
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true, completion: completion)
}
func mapEntriesToParsedItems(account: Account, entries: [ReaderAPIEntry]?) -> Set<ParsedItem> {
@@ -949,7 +956,7 @@ private extension ReaderAPIAccountDelegate {
func deleteTagging(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
func deleteTagging(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
if let folder = container as? Folder, let feedName = feed.subscriptionID {
caller.deleteTagging(subscriptionID: feedName, tagName: folder.name ?? "") { result in
@@ -957,7 +964,7 @@ private extension ReaderAPIAccountDelegate {
case .success:
DispatchQueue.main.async {
self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
folder.removeFeed(feed)
folder.removeWebFeed(feed)
account.addFeedIfNotInAnyFolder(feed)
completion(.success(()))
}
@@ -970,14 +977,14 @@ private extension ReaderAPIAccountDelegate {
}
} else {
if let account = container as? Account {
account.removeFeed(feed)
account.removeWebFeed(feed)
}
completion(.success(()))
}
}
func deleteSubscription(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
func deleteSubscription(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
// This error should never happen
guard let subscriptionID = feed.subscriptionID else {
@@ -989,10 +996,10 @@ private extension ReaderAPIAccountDelegate {
switch result {
case .success:
DispatchQueue.main.async {
account.removeFeed(feed)
account.removeWebFeed(feed)
if let folders = account.folders {
for folder in folders {
folder.removeFeed(feed)
folder.removeWebFeed(feed)
}
}
completion(.success(()))

View File

@@ -78,6 +78,10 @@ final class ReaderAPICaller: NSObject {
self.transport = transport
}
func cancelAll() {
transport.cancelAll()
}
func validateCredentials(endpoint: URL, completion: @escaping (Result<Credentials?, Error>) -> Void) {
guard let credentials = credentials else {
completion(.failure(CredentialsError.incompleteCredentials))
@@ -590,7 +594,7 @@ final class ReaderAPICaller: NSObject {
}
func retrieveEntries(feedID: String, completion: @escaping (Result<([ReaderAPIEntry]?, String?), Error>) -> Void) {
func retrieveEntries(webFeedID: String, completion: @escaping (Result<([ReaderAPIEntry]?, String?), Error>) -> Void) {
let since = Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date()
@@ -602,7 +606,7 @@ final class ReaderAPICaller: NSObject {
let url = baseURL
.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue)
.appendingQueryItems([
URLQueryItem(name: "s", value: feedID),
URLQueryItem(name: "s", value: webFeedID),
URLQueryItem(name: "ot", value: String(since.timeIntervalSince1970)),
URLQueryItem(name: "output", value: "json")
])