Add download feed to folder relationships syncing
This commit is contained in:
parent
ae61d36c7d
commit
52e5e43d10
|
@ -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 }
|
||||
|
||||
|
|
Loading…
Reference in New Issue