diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index b911d2ee8..f5bc4f199 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -36,9 +36,25 @@ public enum AccountType: Int { // TODO: more } -public enum AccountError: Error { +public enum AccountError: LocalizedError { + case createErrorNotFound case createErrorAlreadySubscribed + case opmlImportInProgress + + public var errorDescription: String? { + switch self { + case .opmlImportInProgress: + return NSLocalizedString("An OPML import for this account is already running.", comment: "Import running") + default: + return NSLocalizedString("An unknown error occurred.", comment: "Unknown error") + } + } + + public var recoverySuggestion: String? { + return NSLocalizedString("Please try again later.", comment: "Try later") + } + } public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable { @@ -299,10 +315,18 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } public func importOPML(_ opmlFile: URL, completion: @escaping (Result) -> Void) { + + guard !delegate.opmlImportInProgress else { + completion(.failure(AccountError.opmlImportInProgress)) + return + } + delegate.importOPML(for: self, opmlFile: opmlFile) { [weak self] result in switch result { case .success: guard let self = self else { return } + // Reset the last fetch date to get the article history for the added feeds. + self.metadata.lastArticleFetch = nil self.delegate.refreshAll(for: self) { completion(.success(())) } @@ -310,6 +334,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, completion(.failure(error)) } } + } public func markArticles(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set
? { @@ -544,6 +569,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return database.fetchStarredArticleIDs() } + public func fetchArticleIDsForStatusesWithoutArticles() -> Set { + return database.fetchArticleIDsForStatusesWithoutArticles() + } + public func opmlDocument() -> String { let escapedTitle = nameForDisplay.rs_stringByEscapingSpecialXMLCharacters() let openingText = diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index bb4424072..a86e081b8 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 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 */; }; + 5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5154367A228EEB28005E1CDF /* FeedbinImportResult.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 */; }; @@ -116,6 +117,7 @@ 5133230F22810E5700C30F19 /* FeedbinIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinIcon.swift; sourceTree = ""; }; 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAPICaller.swift; sourceTree = ""; }; 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = ""; }; + 5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinImportResult.swift; sourceTree = ""; }; 5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFolderContentsSyncTest.swift; sourceTree = ""; }; 5165D71322821C2400D9D53D /* taggings_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_delete.json; sourceTree = ""; }; 5165D71422821C2400D9D53D /* taggings_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_add.json; sourceTree = ""; }; @@ -255,6 +257,7 @@ 51E490352288C37100C791F0 /* FeedbinDate.swift */, 84CAD7151FDF2E22000F0755 /* FeedbinEntry.swift */, 5133230F22810E5700C30F19 /* FeedbinIcon.swift */, + 5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */, 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */, 84245C841FDDD8CB0074AFBB /* FeedbinSubscription.swift */, 51D58754227F53BE00900287 /* FeedbinTag.swift */, @@ -521,6 +524,7 @@ 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */, 5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */, 844B297D2106C7EC004020B3 /* Feed.swift in Sources */, + 5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */, 84B2D4D02238CD8A00498ADA /* FeedMetadata.swift in Sources */, 5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */, 84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */, diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index ffdc6e0c0..aaffefc16 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -14,6 +14,8 @@ protocol AccountDelegate { // Local account does not; some synced accounts might. var supportsSubFolders: Bool { get } + var opmlImportInProgress: Bool { get } + var server: String? { get } var credentials: Credentials? { get set } var accountMetadata: AccountMetadata? { get set } diff --git a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift index 17a043433..a2400c7f7 100644 --- a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift +++ b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift @@ -67,6 +67,54 @@ final class FeedbinAPICaller: NSObject { } + func importOPML(opmlData: Data, completion: @escaping (Result) -> Void) { + + let callURL = feedbinBaseURL.appendingPathComponent("imports.json") + let request = URLRequest(url: callURL, credentials: credentials) + + transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in + + switch result { + case .success(let (_, data)): + + guard let resultData = data else { + completion(.failure(TransportError.noData)) + break + } + + do { + let result = try JSONDecoder().decode(FeedbinImportResult.self, from: resultData) + completion(.success(result)) + } catch { + completion(.failure(error)) + } + + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func retrieveOPMLImportResult(importID: Int, completion: @escaping (Result) -> Void) { + + let callURL = feedbinBaseURL.appendingPathComponent("imports/\(importID).json") + let request = URLRequest(url: callURL, credentials: credentials) + + transport.send(request: request, resultType: FeedbinImportResult.self) { result in + + switch result { + case .success(let (_, importResult)): + completion(.success(importResult)) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + func retrieveTags(completion: @escaping (Result<[FeedbinTag]?, Error>) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("tags.json") @@ -299,6 +347,33 @@ final class FeedbinAPICaller: NSObject { } + func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([FeedbinEntry]?), Error>) -> Void) { + + guard !articleIDs.isEmpty else { + completion(.success(([FeedbinEntry]()))) + return + } + + let concatIDs = articleIDs.reduce("") { param, articleID in return param + ",\(articleID)" } + let paramIDs = String(concatIDs.dropFirst()) + + var callURL = URLComponents(url: feedbinBaseURL.appendingPathComponent("entries.json"), resolvingAgainstBaseURL: false)! + callURL.queryItems = [URLQueryItem(name: "ids", value: paramIDs)] + let request = URLRequest(url: callURL.url!, credentials: credentials) + + transport.send(request: request, resultType: [FeedbinEntry].self) { [weak self] result in + + switch result { + case .success(let (_, entries)): + completion(.success((entries))) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + func retrieveEntries(feedID: String, completion: @escaping (Result<([FeedbinEntry]?, String?), Error>) -> Void) { let since = Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date() @@ -359,25 +434,6 @@ final class FeedbinAPICaller: NSObject { } - func extractPageNumber(link: String?) -> Int? { - - guard let link = link else { - return nil - } - - if let lowerBound = link.range(of: "page=")?.upperBound { - if let upperBound = link.range(of: "&")?.lowerBound { - return Int(link[lowerBound..")?.lowerBound { - return Int(link[lowerBound..) -> Void) { guard let callURL = URL(string: page) else { @@ -484,4 +540,23 @@ extension FeedbinAPICaller { } } + func extractPageNumber(link: String?) -> Int? { + + guard let link = link else { + return nil + } + + if let lowerBound = link.range(of: "page=")?.upperBound { + if let upperBound = link.range(of: "&")?.lowerBound { + return Int(link[lowerBound..")?.lowerBound { + return Int(link[lowerBound.. Void)? = nil) { - refreshProgress.addToNumberOfTasksAndRemaining(5) + refreshProgress.addToNumberOfTasksAndRemaining(6) refreshAccount(account) { [weak self] result in switch result { @@ -87,8 +88,12 @@ final class FeedbinAccountDelegate: AccountDelegate { self?.refreshArticles(account) { self?.refreshArticleStatus(for: account) { - self?.refreshProgress.clear() - completion?() + self?.refreshMissingArticles(account) { + self?.refreshProgress.clear() + DispatchQueue.main.async { + completion?() + } + } } } @@ -200,25 +205,32 @@ final class FeedbinAccountDelegate: AccountDelegate { return } - let parserData = ParserData(url: opmlFile.absoluteString, data: opmlData) - var opmlDocument: RSOPMLDocument? + os_log(.debug, log: log, "Begin importing OPML...") + opmlImportInProgress = true - do { - opmlDocument = try RSOPMLParser.parseOPML(with: parserData) - } catch { - completion(.failure(error)) - return + caller.importOPML(opmlData: opmlData) { [weak self] result in + switch result { + case .success(let importResult): + if importResult.complete { + guard let self = self else { return } + os_log(.debug, log: self.log, "Import OPML done.") + self.opmlImportInProgress = false + DispatchQueue.main.async { + completion(.success(())) + } + } else { + self?.checkImportResult(opmlImportResultID: importResult.importResultID, completion: completion) + } + case .failure(let error): + guard let self = self else { return } + os_log(.debug, log: self.log, "Import OPML failed.") + self.opmlImportInProgress = false + DispatchQueue.main.async { + completion(.failure(error)) + } + } } - guard let loadDocument = opmlDocument, let children = loadDocument.children else { - completion(.success(())) - return - } - - importOPMLItems(account, items: children, parentFolder: nil) - - completion(.success(())) - } func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { @@ -507,6 +519,43 @@ private extension FeedbinAccountDelegate { } + func checkImportResult(opmlImportResultID: Int, completion: @escaping (Result) -> Void) { + + DispatchQueue.main.async { + + Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { [weak self] timer in + + guard let self = self else { return } + + os_log(.debug, log: self.log, "Checking status of OPML import...") + + self.caller.retrieveOPMLImportResult(importID: opmlImportResultID) { result in + switch result { + case .success(let importResult): + if let result = importResult, result.complete { + os_log(.debug, log: self.log, "Checking status of OPML import successfully completed.") + timer.invalidate() + self.opmlImportInProgress = false + DispatchQueue.main.async { + completion(.success(())) + } + } + case .failure(let error): + os_log(.debug, log: self.log, "Import OPML check failed.") + timer.invalidate() + self.opmlImportInProgress = false + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } + + } + + } + + } + func syncFolders(_ account: Account, _ tags: [FeedbinTag]?) { guard let tags = tags else { return } @@ -749,125 +798,30 @@ private extension FeedbinAccountDelegate { return } + let group = DispatchGroup() + let articleIDs = statuses.compactMap { Int($0.articleID) } let articleIDGroups = articleIDs.chunked(into: 1000) for articleIDGroup in articleIDGroups { + group.enter() apiCall(articleIDGroup) { [weak self] result in switch result { case .success: self?.database.deleteSelectedForProcessing(articleIDGroup.map { String($0) } ) - completion() + group.leave() case .failure(let error): guard let self = self else { return } os_log(.error, log: self.log, "Article status sync call failed: %@.", error.localizedDescription) self.database.resetSelectedForProcessing(articleIDGroup.map { String($0) } ) - completion() - } - } - - } - } - - func importOPMLItems(_ account: Account, items: [RSOPMLItem], parentFolder: Folder?) { - - items.forEach { (item) in - - if let feedSpecifier = item.feedSpecifier { - importFeedSpecifier(account, feedSpecifier: feedSpecifier, parentFolder: parentFolder) - return - } - - guard let folderName = item.titleFromAttributes else { - // Folder doesn’t have a name, so it won’t be created, and its items will go one level up. - if let itemChildren = item.children { - importOPMLItems(account, items: itemChildren, parentFolder: parentFolder) - } - return - } - - if let folder = account.ensureFolder(with: folderName) { - if let itemChildren = item.children { - importOPMLItems(account, items: itemChildren, parentFolder: folder) + group.leave() } } } - } - - func importFeedSpecifier(_ account: Account, feedSpecifier: RSOPMLFeedSpecifier, parentFolder: Folder?) { - - caller.createSubscription(url: feedSpecifier.feedURL) { [weak self] result in - - switch result { - case .success(let subResult): - switch subResult { - case .created(let sub): - - DispatchQueue.main.async { - - let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL) - feed.subscriptionID = String(sub.subscriptionID) - - self?.importFeedSpecifierPostProcess(account: account, sub: sub, feedSpecifier: feedSpecifier, feed: feed, parentFolder: parentFolder) - - } - - default: - break - } - - case .failure(let error): - guard let self = self else { return } - os_log(.error, log: self.log, "Create feed on OPML import failed: %@.", error.localizedDescription) - } - - } - - } - - func importFeedSpecifierPostProcess(account: Account, sub: FeedbinSubscription, feedSpecifier: RSOPMLFeedSpecifier, feed: Feed, parentFolder: Folder?) { - - // Rename the feed if its name in the OPML file doesn't match the found name - if sub.name != feedSpecifier.title, let newName = feedSpecifier.title { - - self.caller.renameSubscription(subscriptionID: String(sub.subscriptionID), newName: newName) { [weak self] result in - switch result { - case .success: - DispatchQueue.main.async { - feed.editedName = newName - } - case .failure(let error): - guard let self = self else { return } - os_log(.error, log: self.log, "Rename feed on OPML import failed: %@.", error.localizedDescription) - } - } - - } - - // Move the new feed if it is in a folder - if let folder = parentFolder, let feedID = Int(feed.feedID) { - - self.caller.createTagging(feedID: feedID, name: folder.name ?? "") { [weak self] result in - switch result { - case .success(let taggingID): - DispatchQueue.main.async { - self?.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID)) - folder.addFeed(feed) - } - case .failure(let error): - guard let self = self else { return } - os_log(.error, log: self.log, "Move feed to folder on OPML import failed: %@.", error.localizedDescription) - } - } - - } else { - - DispatchQueue.main.async { - account.addFeed(feed) - } - + group.notify(queue: DispatchQueue.main) { + completion() } } @@ -1049,6 +1003,46 @@ private extension FeedbinAccountDelegate { } + func refreshMissingArticles(_ account: Account, completion: @escaping (() -> Void)) { + + os_log(.debug, log: log, "Refreshing missing articles...") + let articleIDs = Array(account.fetchArticleIDsForStatusesWithoutArticles()) + + let group = DispatchGroup() + + let chunkedArticleIDs = articleIDs.chunked(into: 100) + refreshProgress.addToNumberOfTasks(chunkedArticleIDs.count - 1) + + for chunk in chunkedArticleIDs { + + group.enter() + caller.retrieveEntries(articleIDs: chunk) { [weak self] result in + + switch result { + case .success(let entries): + + self?.processEntries(account: account, entries: entries) { + self?.refreshProgress.completeTask() + group.leave() + } + + case .failure(let error): + guard let self = self else { return } + os_log(.error, log: self.log, "Refresh missing articles failed: %@.", error.localizedDescription) + group.leave() + } + + } + + } + + group.notify(queue: DispatchQueue.main) { + os_log(.debug, log: self.log, "Done refreshing missing articles.") + completion() + } + + } + func refreshArticles(_ account: Account, page: String?, completion: @escaping (() -> Void)) { guard let page = page else { diff --git a/Frameworks/Account/Feedbin/FeedbinImportResult.swift b/Frameworks/Account/Feedbin/FeedbinImportResult.swift new file mode 100644 index 000000000..bce437960 --- /dev/null +++ b/Frameworks/Account/Feedbin/FeedbinImportResult.swift @@ -0,0 +1,21 @@ +// +// FeedbinImportResult.swift +// Account +// +// Created by Maurice Parker on 5/17/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct FeedbinImportResult: Codable { + + let importResultID: Int + let complete: Bool + + enum CodingKeys: String, CodingKey { + case importResultID = "id" + case complete + } + +} diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index 150fbf3ea..3e9a97d81 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -18,6 +18,8 @@ public enum LocalAccountDelegateError: String, Error { final class LocalAccountDelegate: AccountDelegate { let supportsSubFolders = false + let opmlImportInProgress = false + let server: String? = nil var credentials: Credentials? var accountMetadata: AccountMetadata? diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index e380c0ed2..6802974b5 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -112,6 +112,10 @@ public final class ArticlesDatabase { return articlesTable.fetchStarredArticleIDs() } + public func fetchArticleIDsForStatusesWithoutArticles() -> Set { + return articlesTable.fetchArticleIDsForStatusesWithoutArticles() + } + public func mark(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set? { return articlesTable.mark(articles, statusKey, flag) } diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index b337f6b1f..05f1a2794 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -307,6 +307,10 @@ final class ArticlesTable: DatabaseTable { return statusesTable.fetchStarredArticleIDs() } + func fetchArticleIDsForStatusesWithoutArticles() -> Set { + return statusesTable.fetchArticleIDsForStatusesWithoutArticles() + } + func mark(_ articles: Set
, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set? { return statusesTable.mark(articles.statuses(), statusKey, flag) diff --git a/Frameworks/ArticlesDatabase/StatusesTable.swift b/Frameworks/ArticlesDatabase/StatusesTable.swift index 080bb4054..86b29034e 100644 --- a/Frameworks/ArticlesDatabase/StatusesTable.swift +++ b/Frameworks/ArticlesDatabase/StatusesTable.swift @@ -86,6 +86,10 @@ final class StatusesTable: DatabaseTable { return fetchArticleIDs("select articleID from statuses where starred=1 and userDeleted=0;") } + func fetchArticleIDsForStatusesWithoutArticles() -> Set { + return fetchArticleIDs("select articleID from statuses s where userDeleted=0 and not exists (select 1 from articles a where a.articleID = s.articleID);") + } + func fetchArticleIDs(_ sql: String) -> Set { var statuses: Set? = nil diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 34be8dd24..8c5e32a98 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 5144EA43227A380F00D19003 /* ExportOPMLWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA42227A380F00D19003 /* ExportOPMLWindowController.swift */; }; 5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */; }; 5144EA52227B8E4500D19003 /* AccountsFeedbin.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */; }; + 51543685228F6753005E1CDF /* DetailAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51543684228F6753005E1CDF /* DetailAccountViewController.swift */; }; 51554C24228B71910055115A /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; }; 51554C25228B71910055115A /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 51554C30228B71A10055115A /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; }; @@ -673,6 +674,7 @@ 5144EA42227A380F00D19003 /* ExportOPMLWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportOPMLWindowController.swift; sourceTree = ""; }; 5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsFeedbinWindowController.swift; sourceTree = ""; }; 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsFeedbin.xib; sourceTree = ""; }; + 51543684228F6753005E1CDF /* DetailAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailAccountViewController.swift; sourceTree = ""; }; 51554BFC228B6EB50055115A /* SyncDatabase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SyncDatabase.xcodeproj; path = Frameworks/SyncDatabase/SyncDatabase.xcodeproj; sourceTree = SOURCE_ROOT; }; 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicLabel.swift; sourceTree = ""; }; 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicImageView.swift; sourceTree = ""; }; @@ -1037,10 +1039,11 @@ 5183CCEC22711DCE0010922C /* Settings.storyboard */, 51E595AA228DF94C00FCC42B /* SettingsTableViewCell.xib */, 5183CCEE227125970010922C /* SettingsViewController.swift */, + 51E595AC228E1C2100FCC42B /* AddAccountViewController.swift */, 51F85BE6227245FC00C787DC /* AboutViewController.swift */, + 51543684228F6753005E1CDF /* DetailAccountViewController.swift */, 51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */, 51EF0F7B2277919E0050506E /* TimelineNumberOfLinesViewController.swift */, - 51E595AC228E1C2100FCC42B /* AddAccountViewController.swift */, ); path = Settings; sourceTree = ""; @@ -2347,6 +2350,7 @@ 51C4526A226508F600C03939 /* MasterFeedTableViewCellLayout.swift in Sources */, 51C452AE2265104D00C03939 /* TimelineStringFormatter.swift in Sources */, 512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */, + 51543685228F6753005E1CDF /* DetailAccountViewController.swift in Sources */, 51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */, 51EF0F802277A8330050506E /* MasterTimelineCellLayout.swift in Sources */, 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, diff --git a/iOS/NavigationStateController.swift b/iOS/NavigationStateController.swift index 6b231be88..25abc5d82 100644 --- a/iOS/NavigationStateController.swift +++ b/iOS/NavigationStateController.swift @@ -177,7 +177,8 @@ class NavigationStateController { NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil) - + NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) @@ -197,6 +198,10 @@ class NavigationStateController { rebuildBackingStores() } + @objc func accountStateDidChange(_ note: Notification) { + rebuildBackingStores() + } + @objc func userDefaultsDidChange(_ note: Notification) { self.sortDirection = AppDefaults.timelineSortDirection } @@ -230,6 +235,8 @@ class NavigationStateController { func rebuildShadowTable() { + shadowTable = [[Node]]() + for i in 0.. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -401,7 +494,7 @@ - + diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index 93691ff39..2e1664f1a 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -48,6 +48,8 @@ class SettingsViewController: UITableViewController { buildLabel.translatesAutoresizingMaskIntoConstraints = false tableView.tableFooterView = buildLabel + tableView.reloadData() + } // MARK: UITableView @@ -96,10 +98,12 @@ class SettingsViewController: UITableViewController { case 0: let sortedAccounts = AccountManager.shared.sortedAccounts if indexPath.row == sortedAccounts.count { - let timeline = UIStoryboard.settings.instantiateController(ofType: AddAccountViewController.self) - self.navigationController?.pushViewController(timeline, animated: true) + let controller = UIStoryboard.settings.instantiateController(ofType: AddAccountViewController.self) + self.navigationController?.pushViewController(controller, animated: true) } else { - // TODO + let controller = UIStoryboard.settings.instantiateController(ofType: DetailAccountViewController.self) + controller.account = sortedAccounts[indexPath.row] + self.navigationController?.pushViewController(controller, animated: true) } case 1: switch indexPath.row {