From cda8acc66c18b23a466d5681ee75fbc39598383d Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 9 May 2019 13:31:18 -0500 Subject: [PATCH] Add the ability to move feeds between folders for Feedbin --- Frameworks/Account/Account.swift | 52 ++++---- Frameworks/Account/AccountDelegate.swift | 5 +- Frameworks/Account/Container.swift | 19 +-- .../Account/Feedbin/FeedbinAPICaller.swift | 41 ++++++ .../Feedbin/FeedbinAccountDelegate.swift | 123 +++++++++++++----- .../Account/Feedbin/FeedbinTagging.swift | 12 ++ Frameworks/Account/Folder.swift | 21 +-- .../LocalAccount/LocalAccountDelegate.swift | 37 +++++- .../AddFeed/AddFeedController.swift | 24 +++- .../Sidebar/SidebarOutlineDataSource.swift | 15 ++- Mac/Scriptability/Feed+Scriptability.swift | 16 ++- Mac/Scriptability/Folder+Scriptability.swift | 8 +- Shared/Commands/DeleteCommand.swift | 10 +- 13 files changed, 261 insertions(+), 122 deletions(-) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 5b78e8e85..2b4edeb62 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -364,22 +364,16 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return true // TODO } - public func addFeed(_ feed: Feed, to folder: Folder?) { - if let folder = folder { - folder.addFeed(feed) - } - else { - addFeed(feed) - } + public func removeFeed(_ feed: Feed, completion: @escaping (Result) -> Void) { + delegate.removeFeed(for: self, from: self, with: feed, completion: completion) } - - public func addFeeds(_ feeds: Set, to folder: Folder?) { - if let folder = folder { - folder.addFeeds(feeds) - } - else { - addFeeds(feeds) - } + + public func addFeed(_ feed: Feed, completion: @escaping (Result) -> Void) { + delegate.addFeed(for: self, to: self, with: feed, completion: completion) + } + + func addFeed(container: Container, feed: Feed, completion: @escaping (Result) -> Void) { + delegate.addFeed(for: self, to: container, with: feed, completion: completion) } public func createFeed(with name: String?, url: String, completion: @escaping (Result) -> Void) { @@ -401,11 +395,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, delegate.renameFeed(for: self, with: feed, to: name, completion: completion) } - public func canAddFolder(_ folder: Folder, to containingFolder: Folder?) -> Bool { - - return false // TODO - } - @discardableResult public func addFolder(_ folder: Folder, to parentFolder: Folder?) -> Bool { @@ -594,21 +583,21 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } public func deleteFeed(_ feed: Feed, completion: @escaping (Result) -> Void) { - delegate.deleteFeed(for: self, container: self, feed: feed, completion: completion) + delegate.deleteFeed(for: self, with: feed, completion: completion) } - func deleteFeed(_ feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { - delegate.deleteFeed(for: self, container: container, feed: feed, completion: completion) + func removeFeed(_ feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { + delegate.removeFeed(for: self, from: container, with: feed, completion: completion) } - func deleteFeed(_ feed: Feed) { + func removeFeed(_ feed: Feed) { topLevelFeeds.remove(feed) structureDidChange() postChildrenDidChangeNotification() } - func deleteFeeds(_ feeds: Set) { - topLevelFeeds.subtract(feeds) + func addFeed(_ feed: Feed) { + topLevelFeeds.insert(feed) structureDidChange() postChildrenDidChangeNotification() } @@ -955,9 +944,16 @@ private extension Account { } } - if !feedsToAdd.isEmpty { - addFeeds(feedsToAdd, to: parentFolder) + if let parentFolder = parentFolder { + for feed in feedsToAdd { + parentFolder.addFeed(feed) + } + } else { + for feed in feedsToAdd { + addFeed(feed) + } } + } func updateUnreadCount() { diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index 6e95b8fc2..2a0cb20ba 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -26,8 +26,11 @@ protocol AccountDelegate { func createFeed(for account: Account, with name: String?, url: String, completion: @escaping (Result) -> Void) func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result) -> Void) - func deleteFeed(for account: Account, container: Container, feed: Feed, completion: @escaping (Result) -> Void) + func deleteFeed(for account: Account, with feed: Feed, completion: @escaping (Result) -> Void) + func addFeed(for account: Account, to container: Container, with: Feed, completion: @escaping (Result) -> Void) + func removeFeed(for account: Account, from container: Container, with: Feed, completion: @escaping (Result) -> Void) + // Called at the end of account’s init method. func accountDidInitialize(_ account: Account) diff --git a/Frameworks/Account/Container.swift b/Frameworks/Account/Container.swift index 6cf310e70..78afa189d 100644 --- a/Frameworks/Account/Container.swift +++ b/Frameworks/Account/Container.swift @@ -27,11 +27,8 @@ public protocol Container: class { func hasChildFolder(with: String) -> Bool func childFolder(with: String) -> Folder? - func deleteFeed(_ feed: Feed, completion: @escaping (Result) -> Void) - func deleteFolder(_ folder: Folder, completion: @escaping (Result) -> Void) - - func addFeed(_ feed: Feed) - func addFeeds(_ feeds: Set) + func removeFeed(_ feed: Feed, completion: @escaping (Result) -> Void) + func addFeed(_ feed: Feed, completion: @escaping (Result) -> Void) //Recursive — checks subfolders func flattenedFeeds() -> Set @@ -47,18 +44,6 @@ public protocol Container: class { public extension Container { - func addFeed(_ feed: Feed) { - addFeeds(Set([feed])) - } - - func addFeeds(_ feeds: Set) { - let feedCount = topLevelFeeds.count - topLevelFeeds.formUnion(feeds) - if feedCount != topLevelFeeds.count { - postChildrenDidChangeNotification() - } - } - func hasAtLeastOneFeed() -> Bool { return topLevelFeeds.count > 0 } diff --git a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift index 7f88720f9..b25758a0b 100644 --- a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift +++ b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift @@ -220,6 +220,47 @@ final class FeedbinAPICaller: NSObject { } + func createTagging(feedID: Int, name: String, completion: @escaping (Result) -> Void) { + + let callURL = feedbinBaseURL.appendingPathComponent("taggings.json") + var request = URLRequest(url: callURL, credentials: credentials) + request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) + + let payload: Data + do { + payload = try JSONEncoder().encode(FeedbinCreateTagging(feedID: feedID, name: name)) + } catch { + completion(.failure(error)) + return + } + + transport.send(request: request, method: HTTPMethod.post, payload:payload) { result in + + switch result { + case .success(let (response, _)): + if let taggingLocation = response.valueForHTTPHeaderField(HTTPResponseHeader.location), + let lowerBound = taggingLocation.range(of: "v2/taggings/")?.upperBound, + let upperBound = taggingLocation.range(of: ".json")?.lowerBound, + let taggingID = Int(taggingLocation[lowerBound..) -> Void) { + let callURL = feedbinBaseURL.appendingPathComponent("taggings/\(taggingID).json") + var request = URLRequest(url: callURL, credentials: credentials) + request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) + transport.send(request: request, method: HTTPMethod.delete, completion: completion) + } + func retrieveIcons(completionHandler completion: @escaping (Result<[FeedbinIcon]?, Error>) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("icons.json") diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index 8fb43441c..c9f855b32 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -97,7 +97,7 @@ final class FeedbinAccountDelegate: AccountDelegate { DispatchQueue.main.sync { BatchUpdate.shared.perform { for feed in folder.topLevelFeeds { - account.addFeed(feed, to: nil) + account.addFeed(feed) self?.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") } account.deleteFolder(folder) @@ -174,7 +174,7 @@ final class FeedbinAccountDelegate: AccountDelegate { } - func deleteFeed(for account: Account, container: Container, feed: Feed, completion: @escaping (Result) -> Void) { + func deleteFeed(for account: Account, with feed: Feed, completion: @escaping (Result) -> Void) { // This error should never happen guard let subscriptionID = feed.subscriptionID else { @@ -186,11 +186,11 @@ final class FeedbinAccountDelegate: AccountDelegate { switch result { case .success: DispatchQueue.main.async { - if let account = container as? Account { - account.deleteFeed(feed) - } - if let folder = container as? Folder { - folder.deleteFeed(feed) + account.removeFeed(feed) + if let folders = account.folders { + for folder in folders { + folder.removeFeed(feed) + } } completion(.success(())) } @@ -203,6 +203,59 @@ final class FeedbinAccountDelegate: AccountDelegate { } + func addFeed(for account: Account, to container: Container, with feed: Feed, completion: @escaping (Result) -> Void) { + + if let folder = container as? Folder, let feedID = Int(feed.feedID) { + caller.createTagging(feedID: feedID, name: folder.name ?? "") { [weak self] result in + switch result { + case .success(let taggingID): + self?.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID)) + DispatchQueue.main.async { + folder.addFeed(feed) + completion(.success(())) + } + case .failure(let error): + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } + } else { + if let account = container as? Account { + account.addFeed(feed) + } + DispatchQueue.main.async { + completion(.success(())) + } + } + + } + + func removeFeed(for account: Account, from container: Container, with feed: Feed, completion: @escaping (Result) -> Void) { + + if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] { + caller.deleteTagging(taggingID: feedTaggingID) { result in + switch result { + case .success: + DispatchQueue.main.async { + folder.removeFeed(feed) + completion(.success(())) + } + case .failure(let error): + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } + } else { + if let account = container as? Account { + account.removeFeed(feed) + } + completion(.success(())) + } + + } + func accountDidInitialize(_ account: Account) { credentials = try? account.retrieveBasicCredentials() accountMetadata = account.metadata @@ -266,7 +319,7 @@ private extension FeedbinAccountDelegate { if !tagNames.contains(folder.name ?? "") { DispatchQueue.main.sync { for feed in folder.topLevelFeeds { - account.addFeed(feed, to: nil) + account.addFeed(feed) clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") } account.deleteFolder(folder) @@ -350,7 +403,7 @@ private extension FeedbinAccountDelegate { for feed in folder.topLevelFeeds { if !subFeedIds.contains(feed.feedID) { DispatchQueue.main.sync { - folder.deleteFeed(feed) + folder.removeFeed(feed) } } } @@ -360,7 +413,7 @@ private extension FeedbinAccountDelegate { for feed in account.topLevelFeeds { if !subFeedIds.contains(feed.feedID) { DispatchQueue.main.sync { - account.deleteFeed(feed) + account.removeFeed(feed) } } } @@ -377,7 +430,7 @@ private extension FeedbinAccountDelegate { } else { let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: subFeedId, homePageURL: subscription.homePageURL) feed.subscriptionID = String(subscription.subscriptionID) - account.addFeed(feed, to: nil) + account.addFeed(feed) } } @@ -422,9 +475,9 @@ private extension FeedbinAccountDelegate { for feed in folder.topLevelFeeds { if !taggingFeedIDs.contains(feed.feedID) { DispatchQueue.main.sync { - folder.deleteFeed(feed) + folder.removeFeed(feed) clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") - account.addFeed(feed, to: nil) + account.addFeed(feed) } } } @@ -432,7 +485,6 @@ private extension FeedbinAccountDelegate { // Add any feeds not in the folder let folderFeedIds = folder.topLevelFeeds.map { $0.feedID } - var feedsToAdd = Set() for tagging in groupedTaggings { let taggingFeedID = String(tagging.feedID) if !folderFeedIds.contains(taggingFeedID) { @@ -440,30 +492,25 @@ private extension FeedbinAccountDelegate { continue } saveFolderRelationship(for: feed, withFolderName: folderName, id: String(tagging.taggingID)) - feedsToAdd.insert(feed) + DispatchQueue.main.sync { + folder.addFeed(feed) + } } } - DispatchQueue.main.sync { - folder.addFeeds(feedsToAdd) - } - } let taggedFeedIDs = Set(taggings.map { String($0.feedID) }) // Remove all feeds from the account container that have a tag - var feedsToDelete = Set() - for feed in account.topLevelFeeds { - if taggedFeedIDs.contains(feed.feedID) { - feedsToDelete.insert(feed) + DispatchQueue.main.sync { + for feed in account.topLevelFeeds { + if taggedFeedIDs.contains(feed.feedID) { + account.removeFeed(feed) + } } } - - DispatchQueue.main.sync { - account.deleteFeeds(feedsToDelete) - } - + } func syncFavicons(_ account: Account, _ icons: [FeedbinIcon]?) { @@ -488,18 +535,22 @@ private extension FeedbinAccountDelegate { } func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) { - if var folderRelationship = feed.folderRelationship { - folderRelationship[folderName] = nil - feed.folderRelationship = folderRelationship + DispatchQueue.main.sync { + if var folderRelationship = feed.folderRelationship { + folderRelationship[folderName] = nil + feed.folderRelationship = folderRelationship + } } } func saveFolderRelationship(for feed: Feed, withFolderName folderName: String, id: String) { - if var folderRelationship = feed.folderRelationship { - folderRelationship[folderName] = id - feed.folderRelationship = folderRelationship - } else { - feed.folderRelationship = [folderName: id] + DispatchQueue.main.sync { + if var folderRelationship = feed.folderRelationship { + folderRelationship[folderName] = id + feed.folderRelationship = folderRelationship + } else { + feed.folderRelationship = [folderName: id] + } } } diff --git a/Frameworks/Account/Feedbin/FeedbinTagging.swift b/Frameworks/Account/Feedbin/FeedbinTagging.swift index c379ae99f..a3f830aec 100644 --- a/Frameworks/Account/Feedbin/FeedbinTagging.swift +++ b/Frameworks/Account/Feedbin/FeedbinTagging.swift @@ -21,3 +21,15 @@ struct FeedbinTagging: Codable { } } + +struct FeedbinCreateTagging: Codable { + + let feedID: Int + let name: String + + enum CodingKeys: String, CodingKey { + case feedID = "feed_id" + case name = "name" + } + +} diff --git a/Frameworks/Account/Folder.swift b/Frameworks/Account/Folder.swift index 4ce26ef20..5bf961fb1 100644 --- a/Frameworks/Account/Folder.swift +++ b/Frameworks/Account/Folder.swift @@ -95,19 +95,24 @@ public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCoun return topLevelFeeds.contains(feed) } - public func deleteFeed(_ feed: Feed, completion: @escaping (Result) -> Void) { - // TODO: Something here... + public func addFeed(_ feed: Feed, completion: @escaping (Result) -> Void) { + account?.addFeed(container: self, feed: feed, completion: completion) } - func deleteFeed(_ feed: Feed) { + public func removeFeed(_ feed: Feed, completion: @escaping (Result) -> Void) { + account?.removeFeed(feed, from: self, completion: completion) + } + + func addFeed(_ feed: Feed) { + topLevelFeeds.insert(feed) + postChildrenDidChangeNotification() + } + + func removeFeed(_ feed: Feed) { topLevelFeeds.remove(feed) postChildrenDidChangeNotification() } - - public func deleteFolder(_ folder: Folder, completion: @escaping (Result) -> Void) { - completion(.success(())) - } - + // MARK: - Hashable public func hash(into hasher: inout Hasher) { diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index 229b1f3ef..3f2938fac 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -65,19 +65,48 @@ final class LocalAccountDelegate: AccountDelegate { completion(.success(())) } - func deleteFeed(for account: Account, container: Container, feed: Feed, completion: @escaping (Result) -> Void) { + func deleteFeed(for account: Account, from container: Container, feed: Feed, completion: @escaping (Result) -> Void) { if let account = container as? Account { - account.deleteFeed(feed) + account.removeFeed(feed) } if let folder = container as? Folder { - folder.deleteFeed(feed) + folder.removeFeed(feed) } completion(.success(())) } - + func deleteFeed(for account: Account, with feed: Feed, completion: @escaping (Result) -> Void) { + account.removeFeed(feed) + if let folders = account.folders { + for folder in folders { + folder.removeFeed(feed) + } + } + completion(.success(())) + } + + func addFeed(for account: Account, to container: Container, with feed: Feed, completion: @escaping (Result) -> Void) { + if let account = container as? Account { + account.addFeed(feed) + } + if let folder = container as? Folder { + folder.addFeed(feed) + } + completion(.success(())) + } + + func removeFeed(for account: Account, from container: Container, with feed: Feed, completion: @escaping (Result) -> Void) { + if let account = container as? Account { + account.removeFeed(feed) + } + if let folder = container as? Folder { + folder.removeFeed(feed) + } + completion(.success(())) + } + func accountDidInitialize(_ account: Account) { } diff --git a/Mac/MainWindow/AddFeed/AddFeedController.swift b/Mac/MainWindow/AddFeed/AddFeedController.swift index c1c71a88f..2264f588a 100644 --- a/Mac/MainWindow/AddFeed/AddFeedController.swift +++ b/Mac/MainWindow/AddFeed/AddFeedController.swift @@ -137,11 +137,25 @@ private extension AddFeedController { } } - // TODO: make this async and add to above code - account.addFeed(feed, to: folder) - - // Move this into the mess above - NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed]) + if let folder = folder { + folder.addFeed(feed) { result in + switch result { + case .success: + NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed]) + case .failure(let error): + NSApplication.shared.presentError(error) + } + } + } else { + account.addFeed(feed) { result in + switch result { + case .success: + NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed]) + case .failure(let error): + NSApplication.shared.presentError(error) + } + } + } } diff --git a/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift b/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift index 359b5f41e..944350e0d 100644 --- a/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift +++ b/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift @@ -242,12 +242,19 @@ private extension SidebarOutlineDataSource { guard let feed = node.representedObject as? Feed else { return } - let sourceContainer = node.parent?.representedObject as? Container - let destinationFolder = parentNode.representedObject as? Folder - sourceContainer?.deleteFeed(feed) { result in + let source = node.parent?.representedObject as? Container + let destination = parentNode.representedObject as? Container + source?.removeFeed(feed) { result in switch result { case .success: - account.addFeed(feed, to: destinationFolder) + destination?.addFeed(feed) { result in + switch result { + case .success: + break + case .failure(let error): + NSApplication.shared.presentError(error) + } + } case .failure(let error): NSApplication.shared.presentError(error) } diff --git a/Mac/Scriptability/Feed+Scriptability.swift b/Mac/Scriptability/Feed+Scriptability.swift index 5e40cae0c..33cb3b04b 100644 --- a/Mac/Scriptability/Feed+Scriptability.swift +++ b/Mac/Scriptability/Feed+Scriptability.swift @@ -113,13 +113,17 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine } // add the feed, putting it in a folder if needed - account.addFeed(feed, to:folder) + account.addFeed(feed) { result in + switch result { + case .success: + NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed]) + let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder) + command.resumeExecution(withResult:scriptableFeed.objectSpecifier) + case .failure: + command.resumeExecution(withResult:nil) + } + } - NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed]) - - let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder) - command.resumeExecution(withResult:scriptableFeed.objectSpecifier) - default: command.resumeExecution(withResult:nil) } diff --git a/Mac/Scriptability/Folder+Scriptability.swift b/Mac/Scriptability/Folder+Scriptability.swift index 64e44b70d..bd215f855 100644 --- a/Mac/Scriptability/Folder+Scriptability.swift +++ b/Mac/Scriptability/Folder+Scriptability.swift @@ -51,13 +51,9 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai } func deleteElement(_ element:ScriptingObject) { - if let scriptableFolder = element as? ScriptableFolder { + if let scriptableFeed = element as? ScriptableFeed { BatchUpdate.shared.perform { - folder.deleteFolder(scriptableFolder.folder) { result in } - } - } else if let scriptableFeed = element as? ScriptableFeed { - BatchUpdate.shared.perform { - folder.deleteFeed(scriptableFeed.feed) { result in } + folder.account?.deleteFeed(scriptableFeed.feed) { result in } } } } diff --git a/Shared/Commands/DeleteCommand.swift b/Shared/Commands/DeleteCommand.swift index 7b0da79ae..cbd0d69e7 100644 --- a/Shared/Commands/DeleteCommand.swift +++ b/Shared/Commands/DeleteCommand.swift @@ -134,12 +134,8 @@ private struct SidebarItemSpecifier { func delete() { - guard let container = container else { - return - } - if let feed = feed { - container.deleteFeed(feed) { result in + account?.deleteFeed(feed) { result in switch result { case .success(): break @@ -152,7 +148,7 @@ private struct SidebarItemSpecifier { } } } else if let folder = folder { - container.deleteFolder(folder) { result in + account?.deleteFolder(folder) { result in switch result { case .success(): break @@ -182,7 +178,7 @@ private struct SidebarItemSpecifier { guard let account = account, let feed = feed else { return } - account.addFeed(feed, to: resolvedFolder()) +// account.addFeed(feed, to: resolvedFolder()) } private func restoreFolder() {