Add download feed to folder relationships syncing

This commit is contained in:
Maurice Parker
2019-05-07 17:41:32 -05:00
parent ae61d36c7d
commit 52e5e43d10
9 changed files with 3594 additions and 40 deletions
+6
View File
@@ -594,6 +594,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return _flattenedFeeds
}
func deleteFeeds(_ feeds: Set<Feed>) {
topLevelFeeds.subtract(feeds)
structureDidChange()
postChildrenDidChangeNotification()
}
public func deleteFeed(_ feed: Feed) {
topLevelFeeds.remove(feed)
structureDidChange()
@@ -17,6 +17,10 @@
5133231122810EB200C30F19 /* FeedbinIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133230F22810E5700C30F19 /* FeedbinIcon.swift */; };
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */; };
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */; };
5165D7122282080C00D9D53D /* AccountFolderContentsSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */; };
5165D71622821C2400D9D53D /* taggings_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71322821C2400D9D53D /* taggings_delete.json */; };
5165D71722821C2400D9D53D /* taggings_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71422821C2400D9D53D /* taggings_add.json */; };
5165D71822821C2400D9D53D /* taggings_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71522821C2400D9D53D /* taggings_initial.json */; };
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; };
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; };
@@ -105,6 +109,10 @@
5133230F22810E5700C30F19 /* FeedbinIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinIcon.swift; sourceTree = "<group>"; };
5144EA48227B497600D19003 /* FeedbinAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAPICaller.swift; sourceTree = "<group>"; };
5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = "<group>"; };
5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFolderContentsSyncTest.swift; sourceTree = "<group>"; };
5165D71322821C2400D9D53D /* taggings_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_delete.json; sourceTree = "<group>"; };
5165D71422821C2400D9D53D /* taggings_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_add.json; sourceTree = "<group>"; };
5165D71522821C2400D9D53D /* taggings_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_initial.json; sourceTree = "<group>"; };
51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; };
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; };
@@ -173,12 +181,15 @@
51D58756227F62E300900287 /* JSON */ = {
isa = PBXGroup;
children = (
5133230D2281089500C30F19 /* icons.json */,
5133230B2281088A00C30F19 /* subscriptions_add.json */,
513323092281082F00C30F19 /* subscriptions_initial.json */,
5165D71422821C2400D9D53D /* taggings_add.json */,
5165D71322821C2400D9D53D /* taggings_delete.json */,
5165D71522821C2400D9D53D /* taggings_initial.json */,
51D58758227F630B00900287 /* tags_add.json */,
51D58757227F630B00900287 /* tags_delete.json */,
51D58759227F630B00900287 /* tags_initial.json */,
513323092281082F00C30F19 /* subscriptions_initial.json */,
5133230B2281088A00C30F19 /* subscriptions_add.json */,
5133230D2281089500C30F19 /* icons.json */,
);
path = JSON;
sourceTree = "<group>";
@@ -280,6 +291,7 @@
5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */,
51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */,
513323072281070C00C30F19 /* AccountFeedSyncTest.swift */,
5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */,
5107A09C227DE77700C7C3C5 /* TestTransport.swift */,
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */,
51D58756227F62E300900287 /* JSON */,
@@ -448,11 +460,14 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5165D71822821C2400D9D53D /* taggings_initial.json in Resources */,
5133230E2281089500C30F19 /* icons.json in Resources */,
51D5875B227F630B00900287 /* tags_add.json in Resources */,
5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */,
51D5875C227F630B00900287 /* tags_initial.json in Resources */,
51D5875A227F630B00900287 /* tags_delete.json in Resources */,
5165D71722821C2400D9D53D /* taggings_add.json in Resources */,
5165D71622821C2400D9D53D /* taggings_delete.json in Resources */,
5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -493,6 +508,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5165D7122282080C00D9D53D /* AccountFolderContentsSyncTest.swift in Sources */,
51D5875E227F643C00900287 /* AccountFolderSyncTest.swift in Sources */,
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */,
513323082281070D00C30F19 /* AccountFeedSyncTest.swift in Sources */,
@@ -43,7 +43,6 @@ class AccountFeedSyncTest: XCTestCase {
// Test Adding a Feed
testTransport.testFiles["https://api.feedbin.com/v2/subscriptions.json"] = "subscriptions_add.json"
// Test initial folders
let addExpection = self.expectation(description: "Add feeds")
account.refreshAll() {
addExpection.fulfill()
@@ -0,0 +1,68 @@
//
// AccountFolderContentsSyncTest.swift
// AccountTests
//
// Created by Maurice Parker on 5/7/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
class AccountFolderContentsSyncTest: XCTestCase {
override func setUp() {
}
override func tearDown() {
}
func testDownloadSync() {
let testTransport = TestTransport()
testTransport.testFiles["https://api.feedbin.com/v2/tags.json"] = "tags_add.json"
testTransport.testFiles["https://api.feedbin.com/v2/subscriptions.json"] = "subscriptions_initial.json"
testTransport.testFiles["https://api.feedbin.com/v2/taggings.json"] = "taggings_initial.json"
testTransport.testFiles["https://api.feedbin.com/v2/icons.json"] = "icons.json"
let account = TestAccountManager.shared.createAccount(type: .feedbin, transport: testTransport)
// Test initial folders
let initialExpection = self.expectation(description: "Initial contents")
account.refreshAll() {
initialExpection.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
let folder = account.folders?.filter { $0.name == "Developers" } .first!
XCTAssertEqual(156, folder?.topLevelFeeds.count ?? 0)
XCTAssertEqual(2, account.topLevelFeeds.count)
// Test Adding a Feed to the folder
testTransport.testFiles["https://api.feedbin.com/v2/taggings.json"] = "taggings_add.json"
let addExpection = self.expectation(description: "Add contents")
account.refreshAll() {
addExpection.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
XCTAssertEqual(157, folder?.topLevelFeeds.count ?? 0)
XCTAssertEqual(1, account.topLevelFeeds.count)
// Test Deleting some Feeds from the folder
testTransport.testFiles["https://api.feedbin.com/v2/taggings.json"] = "taggings_delete.json"
let deleteExpection = self.expectation(description: "Delete contents")
account.refreshAll() {
deleteExpection.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
XCTAssertEqual(153, folder?.topLevelFeeds.count ?? 0)
XCTAssertEqual(5, account.topLevelFeeds.count)
TestAccountManager.shared.deleteAccount(account)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -113,6 +113,26 @@ final class FeedbinAPICaller: NSObject {
}
func retrieveTaggings(completionHandler completion: @escaping (Result<[FeedbinTagging]?, Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("taggings.json")
let conditionalGet = accountMetadata?.conditionalGetInfo[AccountMetadata.ConditionalGetKeys.taggings]
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
transport.send(request: request, resultType: [FeedbinTagging].self) { [weak self] result in
switch result {
case .success(let (headers, taggings)):
self?.storeConditionalGet(metadata: self?.accountMetadata, key: AccountMetadata.ConditionalGetKeys.taggings, headers: headers)
completion(.success(taggings))
case .failure(let error):
completion(.failure(error))
}
}
}
func retrieveIcons(completionHandler completion: @escaping (Result<[FeedbinIcon]?, Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("icons.json")
@@ -81,16 +81,22 @@ final class FeedbinAccountDelegate: AccountDelegate {
return
}
caller.deleteTag(name: folder.name ?? "") { result in
// After we successfully delete at Feedbin, we add all the feeds to the account to save them. We then
// delete the folder. We then sync the taggings we received on the delete to remove any feeds from
// the account that might be in another folder.
caller.deleteTag(name: folder.name ?? "") { [weak self] result in
switch result {
case .success:
DispatchQueue.main.async {
account.deleteFolder(folder)
// TODO: Take the serialized taggings and reestablish the folder to feed relationships. Deleting
// a tag on Feedbin doesn't any feeds.
case .success(let taggings):
DispatchQueue.main.sync {
BatchUpdate.shared.perform {
for feed in folder.topLevelFeeds {
account.addFeed(feed, to: nil)
}
account.deleteFolder(folder)
}
completion(.success(()))
}
self?.syncTaggings(account, taggings)
case .failure(let error):
DispatchQueue.main.async {
completion(.failure(error))
@@ -160,6 +166,9 @@ private extension FeedbinAccountDelegate {
folders.forEach { folder in
if !tagNames.contains(folder.name ?? "") {
DispatchQueue.main.sync {
for feed in folder.topLevelFeeds {
account.addFeed(feed, to: nil)
}
account.deleteFolder(folder)
}
}
@@ -186,58 +195,168 @@ private extension FeedbinAccountDelegate {
}
func refreshFeeds(_ account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
caller.retrieveSubscriptions { [weak self] result in
switch result {
case .success(let subscriptions):
self?.syncFeeds(account, subscriptions)
self?.refreshFavicons(account, completion: completion)
self?.caller.retrieveTaggings { [weak self] result in
switch result {
case .success(let taggings):
self?.caller.retrieveIcons { [weak self] result in
switch result {
case .success(let icons):
BatchUpdate.shared.perform {
self?.syncFeeds(account, subscriptions)
self?.syncTaggings(account, taggings)
self?.syncFavicons(account, icons)
}
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
}
func syncFeeds(_ account: Account, _ subscriptions: [FeedbinSubscription]?) {
guard let subscriptions = subscriptions else { return }
BatchUpdate.shared.perform {
subscriptions.forEach { subscription in
syncFeed(account, subscription)
let subFeedIds = subscriptions.map { String($0.feedID) }
// 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) {
DispatchQueue.main.sync {
folder.deleteFeed(feed)
}
}
}
}
}
for feed in account.topLevelFeeds {
if !subFeedIds.contains(feed.feedID) {
DispatchQueue.main.sync {
account.deleteFeed(feed)
}
}
}
// Add any feeds we don't have and update any we do
subscriptions.forEach { subscription in
let subFeedId = String(subscription.feedID)
DispatchQueue.main.sync {
if let feed = account.idToFeedDictionary[subFeedId] {
feed.name = subscription.name
feed.homePageURL = subscription.homePageURL
} else {
let feed = account.createFeed(with: subscription.name, editedName: nil, url: subscription.url, feedId: subFeedId, homePageURL: subscription.homePageURL)
account.addFeed(feed, to: nil)
}
}
}
}
func syncFeed(_ account: Account, _ subscription: FeedbinSubscription) {
func syncTaggings(_ account: Account, _ taggings: [FeedbinTagging]?) {
let subFeedId = String(subscription.feedID)
guard let taggings = taggings else { return }
// Set up some structures to make syncing easier
let folderDict: [String: Folder] = {
if let folders = account.folders {
return Dictionary(uniqueKeysWithValues: folders.map { ($0.name ?? "", $0) } )
} else {
return [String: Folder]()
}
}()
let taggingsDict = taggings.reduce([String: [String]]()) { (dict, tagging) in
var taggedFeeds = dict
if var taggedFeed = taggedFeeds[tagging.name] {
taggedFeed.append(String(tagging.feedID))
taggedFeeds[tagging.name] = taggedFeed
} else {
taggedFeeds[tagging.name] = [String(tagging.feedID)]
}
return taggedFeeds
}
// Sync the folders
for (folderName, feedIDs) in taggingsDict {
guard let folder = folderDict[folderName] else { return }
// Move any feeds not in the folder to the account
for feed in folder.topLevelFeeds {
if !feedIDs.contains(feed.feedID) {
DispatchQueue.main.sync {
folder.deleteFeed(feed)
account.addFeed(feed, to: nil)
}
}
}
// Add any feeds not in the folder
let folderFeedIds = folder.topLevelFeeds.map { $0.feedID }
var feedsToAdd = Set<Feed>()
for feedId in feedIDs {
if !folderFeedIds.contains(feedId) {
guard let feed = account.idToFeedDictionary[feedId] else {
continue
}
feedsToAdd.insert(feed)
}
}
DispatchQueue.main.sync {
folder.addFeeds(feedsToAdd)
}
}
let taggedFeedIds = Set(taggings.map { String($0.feedID) })
// Delete all the feeds without a tag
var feedsToDelete = Set<Feed>()
for feed in account.topLevelFeeds {
if taggedFeedIds.contains(feed.feedID) {
feedsToDelete.insert(feed)
}
}
DispatchQueue.main.sync {
if let feed = account.idToFeedDictionary[subFeedId] {
feed.name = subscription.name
feed.homePageURL = subscription.homePageURL
} else {
let feed = account.createFeed(with: subscription.name, editedName: nil, url: subscription.url, feedId: subFeedId, homePageURL: subscription.homePageURL)
account.addFeed(feed, to: nil)
}
}
}
func refreshFavicons(_ account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
caller.retrieveIcons { [weak self] result in
switch result {
case .success(let icons):
self?.syncIcons(account, icons)
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
account.deleteFeeds(feedsToDelete)
}
}
func syncIcons(_ account: Account, _ icons: [FeedbinIcon]?) {
func syncFavicons(_ account: Account, _ icons: [FeedbinIcon]?) {
guard let icons = icons else { return }