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

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()

View File

@ -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 */,

View File

@ -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()

View File

@ -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

View File

@ -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")

View File

@ -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 }