diff --git a/Frameworks/Account/Feed.swift b/Frameworks/Account/Feed.swift index 5b416bcf8..c0b3cb9cd 100644 --- a/Frameworks/Account/Feed.swift +++ b/Frameworks/Account/Feed.swift @@ -64,7 +64,7 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha set { let oldNameForDisplay = nameForDisplay metadata.name = newValue - if oldNameForDisplay != nameForDisplay { + if oldNameForDisplay != newValue { postDisplayNameDidChangeNotification() } } diff --git a/Frameworks/Account/FeedFinder/FeedSpecifier.swift b/Frameworks/Account/FeedFinder/FeedSpecifier.swift index 68073198e..f90e956fe 100644 --- a/Frameworks/Account/FeedFinder/FeedSpecifier.swift +++ b/Frameworks/Account/FeedFinder/FeedSpecifier.swift @@ -46,7 +46,7 @@ struct FeedSpecifier: Hashable { return feedSpecifiers.anyObject() } - var currentHighScore = 0 + var currentHighScore = Int.min var currentBestFeed: FeedSpecifier? = nil for oneFeedSpecifier in feedSpecifiers { diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index f7788af52..f6711fc8a 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -88,11 +88,13 @@ final class FeedbinAccountDelegate: AccountDelegate { case .success(): self.refreshArticles(account) { - self.refreshArticleStatus(for: account) { - self.refreshMissingArticles(account) { - self.refreshProgress.clear() - DispatchQueue.main.async { - completion(.success(())) + self.sendArticleStatus(for: account) { + self.refreshArticleStatus(for: account) { + self.refreshMissingArticles(account) { + self.refreshProgress.clear() + DispatchQueue.main.async { + completion(.success(())) + } } } } @@ -241,10 +243,16 @@ final class FeedbinAccountDelegate: AccountDelegate { func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { + guard folder.hasAtLeastOneFeed() else { + folder.name = name + return + } + caller.renameTag(oldName: folder.name ?? "", newName: name) { result in switch result { case .success: DispatchQueue.main.async { + self.renameFolderRelationship(for: account, fromName: folder.name ?? "", toName: name) folder.name = name completion(.success(())) } @@ -269,16 +277,44 @@ final class FeedbinAccountDelegate: AccountDelegate { let group = DispatchGroup() for feed in folder.topLevelFeeds { - group.enter() - removeFeed(for: account, with: feed, from: folder) { result in - group.leave() - switch result { - case .success: - break - case .failure(let error): - os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription) + + if feed.folderRelationship?.count ?? 0 > 1 { + + if let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] { + group.enter() + caller.deleteTagging(taggingID: feedTaggingID) { result in + group.leave() + switch result { + case .success: + DispatchQueue.main.async { + self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") + } + case .failure(let error): + os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription) + } + } } + + } else { + + if let subscriptionID = feed.subscriptionID { + group.enter() + caller.deleteSubscription(subscriptionID: subscriptionID) { result in + group.leave() + switch result { + case .success: + DispatchQueue.main.async { + account.clearFeedMetadata(feed) + } + case .failure(let error): + os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription) + } + } + + } + } + } group.notify(queue: DispatchQueue.main) { @@ -347,7 +383,6 @@ final class FeedbinAccountDelegate: AccountDelegate { if feed.folderRelationship?.count ?? 0 > 1 { deleteTagging(for: account, with: feed, from: container, completion: completion) } else { - account.clearFeedMetadata(feed) deleteSubscription(for: account, with: feed, from: container, completion: completion) } } @@ -399,12 +434,23 @@ final class FeedbinAccountDelegate: AccountDelegate { func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result) -> Void) { - createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in - switch result { - case .success: - completion(.success(())) - case .failure(let error): - completion(.failure(error)) + if let existingFeed = account.existingFeed(withURL: feed.url) { + account.addFeed(existingFeed, to: container) { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } + } else { + createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } } } @@ -412,22 +458,27 @@ final class FeedbinAccountDelegate: AccountDelegate { func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> Void) { - account.addFolder(folder) let group = DispatchGroup() for feed in folder.topLevelFeeds { + folder.topLevelFeeds.remove(feed) + group.enter() - addFeed(for: account, with: feed, to: folder) { result in - if account.topLevelFeeds.contains(feed) { - account.removeFeed(feed) - } + restoreFeed(for: account, feed: feed, container: folder) { result in group.leave() + switch result { + case .success: + break + case .failure(let error): + os_log(.error, log: self.log, "Restore folder feed error: %@.", error.localizedDescription) + } } } group.notify(queue: DispatchQueue.main) { + account.addFolder(folder) completion(.success(())) } @@ -647,7 +698,10 @@ private extension FeedbinAccountDelegate { DispatchQueue.main.sync { if let feed = account.idToFeedDictionary[subFeedId] { feed.name = subscription.name + // If the name has been changed on the server remove the locally edited name + feed.editedName = nil feed.homePageURL = subscription.homePageURL + feed.subscriptionID = String(subscription.subscriptionID) } else { let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: subFeedId, homePageURL: subscription.homePageURL) feed.subscriptionID = String(subscription.subscriptionID) @@ -792,6 +846,17 @@ private extension FeedbinAccountDelegate { } + func renameFolderRelationship(for account: Account, fromName: String, toName: String) { + for feed in account.flattenedFeeds() { + if var folderRelationship = feed.folderRelationship { + let relationship = folderRelationship[fromName] + folderRelationship[fromName] = nil + folderRelationship[toName] = relationship + feed.folderRelationship = folderRelationship + } + } + } + func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) { if var folderRelationship = feed.folderRelationship { folderRelationship[folderName] = nil @@ -1158,6 +1223,7 @@ private extension FeedbinAccountDelegate { switch result { case .success: DispatchQueue.main.async { + account.clearFeedMetadata(feed) account.removeFeed(feed) if let folders = account.folders { for folder in folders { diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index aab4c1f56..22cd0be15 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -93,41 +93,53 @@ final class LocalAccountDelegate: AccountDelegate { return } - FeedFinder.find(url: url) { result in + DispatchQueue.global(qos: .userInitiated).async { - switch result { - case .success(let feedSpecifiers): + FeedFinder.find(url: url) { result in - guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), - let url = URL(string: bestFeedSpecifier.urlString) else { - completion(.failure(AccountError.createErrorNotFound)) - return - } - - if account.hasFeed(withURL: bestFeedSpecifier.urlString) { - completion(.failure(AccountError.createErrorAlreadySubscribed)) - return - } - - let feed = account.createFeed(with: nil, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil) - - InitialFeedDownloader.download(url) { parsedFeed in + switch result { + case .success(let feedSpecifiers): - if let parsedFeed = parsedFeed { - account.update(feed, with: parsedFeed, {}) + guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), + let url = URL(string: bestFeedSpecifier.urlString) else { + DispatchQueue.main.async { + completion(.failure(AccountError.createErrorNotFound)) + } + return } - feed.editedName = name + if account.hasFeed(withURL: bestFeedSpecifier.urlString) { + DispatchQueue.main.async { + completion(.failure(AccountError.createErrorAlreadySubscribed)) + } + return + } - container.addFeed(feed) - completion(.success(feed)) + let feed = account.createFeed(with: nil, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil) + InitialFeedDownloader.download(url) { parsedFeed in + + if let parsedFeed = parsedFeed { + account.update(feed, with: parsedFeed, {}) + } + + feed.editedName = name + + container.addFeed(feed) + DispatchQueue.main.async { + completion(.success(feed)) + } + + } + + case .failure: + DispatchQueue.main.async { + completion(.failure(AccountError.createErrorNotFound)) + } } - case .failure(let error): - completion(.failure(error)) } - + } } diff --git a/Mac/Base.lproj/Main.storyboard b/Mac/Base.lproj/Main.storyboard index 83e29b174..fe81915b1 100644 --- a/Mac/Base.lproj/Main.storyboard +++ b/Mac/Base.lproj/Main.storyboard @@ -413,7 +413,7 @@ - + diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift index 7643b1091..c37b49793 100644 --- a/Shared/Article Rendering/ArticleRenderer.swift +++ b/Shared/Article Rendering/ArticleRenderer.swift @@ -86,10 +86,11 @@ private extension ArticleRenderer { } func titleOrTitleLink() -> String { + let escapedTitle = title.escapeHTML() if let link = article?.preferredLink { - return title.htmlByAddingLink(link) + return escapedTitle.htmlByAddingLink(link) } - return title + return escapedTitle } func substitutions() -> [String: String] { diff --git a/Shared/Commands/DeleteCommand.swift b/Shared/Commands/DeleteCommand.swift index 6a8a15a5d..cb7f6f3cc 100644 --- a/Shared/Commands/DeleteCommand.swift +++ b/Shared/Commands/DeleteCommand.swift @@ -46,12 +46,30 @@ final class DeleteCommand: UndoableCommand { func perform() { BatchUpdate.shared.perform { - itemSpecifiers.forEach { $0.delete() } + itemSpecifiers.forEach { $0.delete() {} } treeController.rebuild() } registerUndo() } + func perform(completion: @escaping () -> Void) { + + let group = DispatchGroup() + group.enter() + itemSpecifiers.forEach { + $0.delete() { + group.leave() + } + } + treeController.rebuild() + + group.notify(queue: DispatchQueue.main) { + self.registerUndo() + completion() + } + + } + func undo() { BatchUpdate.shared.perform { @@ -132,18 +150,20 @@ private struct SidebarItemSpecifier { self.path = ContainerPath(account: account!, folders: node.containingFolders()) } - func delete() { + func delete(completion: @escaping () -> Void) { if let feed = feed { BatchUpdate.shared.start() account?.removeFeed(feed, from: path.resolveContainer()) { result in BatchUpdate.shared.end() + completion() self.checkResult(result) } } else if let folder = folder { BatchUpdate.shared.start() account?.removeFolder(folder) { result in BatchUpdate.shared.end() + completion() self.checkResult(result) } } diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 4f01ed04d..809666f11 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -526,14 +526,23 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn else { return } - - navState.beginUpdates() - runCommand(deleteCommand) - navState.rebuildShadowTable() - tableView.deleteRows(at: [indexPath], with: .automatic) + var deleteIndexPaths = [indexPath] + if navState.isExpanded(deleteNode) { + for i in 0.. Int { + + guard let account = account else { return 0 } + if account == AccountManager.shared.defaultAccount { return 1 + } else if account.type == .onMyMac { + return 2 } else { return super.numberOfSections(in: tableView) } } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = super.tableView(tableView, cellForRowAt: indexPath) + + let cell: UITableViewCell + if indexPath.section == 1, let account = account, account.type == .onMyMac { + cell = super.tableView(tableView, cellForRowAt: IndexPath(row: 0, section: 2)) + } else { + cell = super.tableView(tableView, cellForRowAt: indexPath) + } let bgView = UIView() bgView.backgroundColor = AppAssets.selectionBackgroundColor @@ -54,7 +65,7 @@ extension DetailAccountViewController { } override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { - if indexPath.section == 1 { + if indexPath.section > 0 { return true } @@ -62,8 +73,19 @@ extension DetailAccountViewController { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if indexPath.section == 1 { - deleteAccount() + if let account = account, account.type == .onMyMac { + if indexPath.section == 1 { + deleteAccount() + } + } else { + switch indexPath.section { + case 1: + credentials() + case 2: + deleteAccount() + default: + break + } } tableView.selectRow(at: nil, animated: true, scrollPosition: .none) @@ -73,6 +95,19 @@ extension DetailAccountViewController { private extension DetailAccountViewController { + func credentials() { + guard let account = account else { return } + switch account.type { + case .feedbin: + let navController = UIStoryboard.settings.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController + let addViewController = navController.topViewController as! FeedbinAccountViewController + addViewController.account = account + present(navController, animated: true) + default: + break + } + } + func deleteAccount() { let title = NSLocalizedString("Delete Account", comment: "Delete Account") let message = NSLocalizedString("Are you sure you want to delete this account? This can not be undone.", comment: "Delete Account") diff --git a/iOS/Settings/FeedbinAccountViewController.swift b/iOS/Settings/FeedbinAccountViewController.swift index d826c5086..c12d3d941 100644 --- a/iOS/Settings/FeedbinAccountViewController.swift +++ b/iOS/Settings/FeedbinAccountViewController.swift @@ -16,7 +16,7 @@ class FeedbinAccountViewController: UIViewController { @IBOutlet weak var cancelBarButtonItem: UIBarButtonItem! @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! - @IBOutlet weak var addAccountButton: UIButton! + @IBOutlet weak var actionButton: UIButton! @IBOutlet weak var errorMessageLabel: UILabel! @@ -31,18 +31,22 @@ class FeedbinAccountViewController: UIViewController { passwordTextField.delegate = self if let account = account, let credentials = try? account.retrieveBasicCredentials() { + actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal) if case .basic(let username, let password) = credentials { emailTextField.text = username passwordTextField.text = password } + } else { + actionButton.setTitle(NSLocalizedString("Add Account", comment: "Update Credentials"), for: .normal) } } @IBAction func cancel(_ sender: Any) { - delegate?.dismiss(self) + dismiss(animated: true, completion: nil) + delegate?.dismiss() } - @IBAction func addAccountTapped(_ sender: Any) { + @IBAction func action(_ sender: Any) { self.errorMessageLabel.text = nil guard emailTextField.text != nil && passwordTextField.text != nil else { @@ -56,8 +60,7 @@ class FeedbinAccountViewController: UIViewController { // When you fill in the email address via auto-complete it adds extra whitespace let emailAddress = emailTextField.text?.trimmingCharacters(in: .whitespaces) let credentials = Credentials.basic(username: emailAddress ?? "", password: passwordTextField.text ?? "") - Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in - guard let self = self else { return } + Account.validateCredentials(type: .feedbin, credentials: credentials) { result in self.stopAnimtatingActivityIndicator() self.enableNavigation() @@ -73,7 +76,9 @@ class FeedbinAccountViewController: UIViewController { do { - try self.account?.removeBasicCredentials() + do { + try self.account?.removeBasicCredentials() + } catch {} try self.account?.storeCredentials(credentials) if newAccount { @@ -87,7 +92,8 @@ class FeedbinAccountViewController: UIViewController { } } - self.delegate?.dismiss(self) + self.dismiss(animated: true, completion: nil) + self.delegate?.dismiss() } catch { self.errorMessageLabel.text = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") } @@ -103,12 +109,12 @@ class FeedbinAccountViewController: UIViewController { private func enableNavigation() { self.cancelBarButtonItem.isEnabled = true - self.addAccountButton.isEnabled = true + self.actionButton.isEnabled = true } private func disableNavigation() { cancelBarButtonItem.isEnabled = false - addAccountButton.isEnabled = false + actionButton.isEnabled = false } private func startAnimatingActivityIndicator() { diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index 462b663f1..60212b3b2 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -368,7 +368,31 @@ - + + + + + + + + + + + + + + + + + + - - + + @@ -658,10 +682,9 @@ - + - - +