Add the ability to move feeds between folders for Feedbin

This commit is contained in:
Maurice Parker 2019-05-09 13:31:18 -05:00
parent e45362bffc
commit cda8acc66c
13 changed files with 261 additions and 122 deletions

View File

@ -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, Error>) -> Void) {
delegate.removeFeed(for: self, from: self, with: feed, completion: completion)
}
public func addFeeds(_ feeds: Set<Feed>, to folder: Folder?) {
if let folder = folder {
folder.addFeeds(feeds)
}
else {
addFeeds(feeds)
}
public func addFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
delegate.addFeed(for: self, to: self, with: feed, completion: completion)
}
func addFeed(container: Container, feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
delegate.addFeed(for: self, to: container, with: feed, completion: completion)
}
public func createFeed(with name: String?, url: String, completion: @escaping (Result<AccountCreateFeedResult, Error>) -> 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, Error>) -> 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, Error>) -> Void) {
delegate.deleteFeed(for: self, container: container, feed: feed, completion: completion)
func removeFeed(_ feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> 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<Feed>) {
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() {

View File

@ -26,8 +26,11 @@ protocol AccountDelegate {
func createFeed(for account: Account, with name: String?, url: String, completion: @escaping (Result<AccountCreateFeedResult, Error>) -> Void)
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
func deleteFeed(for account: Account, container: Container, feed: Feed, completion: @escaping (Result<Void, Error>) -> Void)
func deleteFeed(for account: Account, with feed: Feed, completion: @escaping (Result<Void, Error>) -> Void)
func addFeed(for account: Account, to container: Container, with: Feed, completion: @escaping (Result<Void, Error>) -> Void)
func removeFeed(for account: Account, from container: Container, with: Feed, completion: @escaping (Result<Void, Error>) -> Void)
// Called at the end of accounts init method.
func accountDidInitialize(_ account: Account)

View File

@ -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, Error>) -> Void)
func deleteFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
func addFeed(_ feed: Feed)
func addFeeds(_ feeds: Set<Feed>)
func removeFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void)
func addFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void)
//Recursive  checks subfolders
func flattenedFeeds() -> Set<Feed>
@ -47,18 +44,6 @@ public protocol Container: class {
public extension Container {
func addFeed(_ feed: Feed) {
addFeeds(Set([feed]))
}
func addFeeds(_ feeds: Set<Feed>) {
let feedCount = topLevelFeeds.count
topLevelFeeds.formUnion(feeds)
if feedCount != topLevelFeeds.count {
postChildrenDidChangeNotification()
}
}
func hasAtLeastOneFeed() -> Bool {
return topLevelFeeds.count > 0
}

View File

@ -220,6 +220,47 @@ final class FeedbinAPICaller: NSObject {
}
func createTagging(feedID: Int, name: String, completion: @escaping (Result<Int, Error>) -> 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..<upperBound]) {
completion(.success(taggingID))
} else {
completion(.failure(TransportError.noData))
}
case .failure(let error):
completion(.failure(error))
}
}
}
func deleteTagging(taggingID: String, completion: @escaping (Result<Void, Error>) -> 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")

View File

@ -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, Error>) -> Void) {
func deleteFeed(for account: Account, with feed: Feed, completion: @escaping (Result<Void, Error>) -> 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, Error>) -> 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, Error>) -> 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<Feed>()
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<Feed>()
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]
}
}
}

View File

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

View File

@ -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, Error>) -> Void) {
// TODO: Something here...
public func addFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
account?.addFeed(container: self, feed: feed, completion: completion)
}
func deleteFeed(_ feed: Feed) {
public func removeFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> 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, Error>) -> Void) {
completion(.success(()))
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {

View File

@ -65,19 +65,48 @@ final class LocalAccountDelegate: AccountDelegate {
completion(.success(()))
}
func deleteFeed(for account: Account, container: Container, feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
func deleteFeed(for account: Account, from container: Container, feed: Feed, completion: @escaping (Result<Void, Error>) -> 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, Error>) -> 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, Error>) -> 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, Error>) -> 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) {
}

View File

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

View File

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

View File

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

View File

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

View File

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