diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ec5b8ce8..4c8984b2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,4 +49,14 @@ jobs: SCHEME: ${{ matrix.run-config['scheme'] }} DESTINATION: ${{ matrix.run-config['destination'] }} - run: buildscripts/ci-build.sh \ No newline at end of file + run: buildscripts/ci-build.sh + + - name: Notify Slack + uses: 8398a7/action-slack@v2.4.2 + with: + status: ${{ job.status }} + author_name: GitHub Actions CI + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + if: failure() \ No newline at end of file diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index f40d48c43..597fd920e 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -308,19 +308,16 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } - public static func oauthAuthorizationClient(for type: AccountType) -> OAuthAuthorizationClient { - let grantingType: OAuthAuthorizationGranting.Type + internal static func oauthAuthorizationClient(for type: AccountType) -> OAuthAuthorizationClient { switch type { case .feedly: - grantingType = FeedlyAccountDelegate.self + return FeedlyAccountDelegate.environment.oauthAuthorizationClient default: - fatalError("\(type) does not support OAuth authorization code granting.") + fatalError("\(type) is not a client for OAuth authorization code granting.") } - - return grantingType.oauthAuthorizationClient } - - public static func oauthAuthorizationCodeGrantRequest(for type: AccountType, client: OAuthAuthorizationClient) -> URLRequest { + + public static func oauthAuthorizationCodeGrantRequest(for type: AccountType) -> URLRequest { let grantingType: OAuthAuthorizationGranting.Type switch type { case .feedly: @@ -329,7 +326,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, fatalError("\(type) does not support OAuth authorization code granting.") } - return grantingType.oauthAuthorizationCodeGrantRequest(for: client) + return grantingType.oauthAuthorizationCodeGrantRequest() } public static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, @@ -346,7 +343,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, fatalError("\(accountType) does not support OAuth authorization code granting.") } - grantingType.requestOAuthAccessToken(with: response, client: client, transport: transport, completionHandler: completionHandler) + grantingType.requestOAuthAccessToken(with: response, transport: transport, completionHandler: completionHandler) } public func refreshAll(completion: @escaping (Result) -> Void) { diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index e0f88b7d8..8044c4679 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -120,6 +120,8 @@ 9E85C8E82366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */; }; 9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */; }; 9E85C8ED2367020700D0F1F7 /* FeedlyGetEntriesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */; }; + 9E964EB823754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */; }; + 9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */; }; 9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */; }; 9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; }; 9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */; }; @@ -320,6 +322,8 @@ 9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetPagedStreamContentsService.swift; sourceTree = ""; }; 9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesOperation.swift; sourceTree = ""; }; 9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesService.swift; sourceTree = ""; }; + 9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OAuthAuthorizationClient+Feedly.swift"; sourceTree = ""; }; + 9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAccountAuthorizationOperation.swift; sourceTree = ""; }; 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegate.swift; sourceTree = ""; }; 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCollection.swift; sourceTree = ""; }; 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeed.swift; sourceTree = ""; }; @@ -616,7 +620,9 @@ 9EE4CCF9234F106600FBAE4B /* FeedlyFeedContainerValidator.swift */, 9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */, 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */, + 9E964E9E23754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift */, 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */, + 9E964EB923754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift */, 9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */, 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */, 9E510D6D234F16A8002E6F1A /* FeedlyAddFeedRequest.swift */, @@ -716,7 +722,9 @@ isa = PBXNativeTarget; buildConfigurationList = 8489350A1F62485000CEBD24 /* Build configuration list for PBXNativeTarget "Account" */; buildPhases = ( + 9E964EBB2375512300A7AF2E /* Run Script: Update OAuthAuthorizationClient+Feedly.swift */, 848934F11F62484F00CEBD24 /* Sources */, + 9E964EBC2375517100A7AF2E /* Run Script: Reset OAuthAuthorizationClient+Feedly.swift */, 848934F21F62484F00CEBD24 /* Frameworks */, 848934F31F62484F00CEBD24 /* Headers */, 848934F41F62484F00CEBD24 /* Resources */, @@ -889,6 +897,42 @@ shellPath = /bin/sh; shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 ../../buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n"; }; + 9E964EBB2375512300A7AF2E /* Run Script: Update OAuthAuthorizationClient+Feedly.swift */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Script: Update OAuthAuthorizationClient+Feedly.swift"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "FAILED=false\n\nif [ -z \"${FEEDLY_CLIENT_ID}\" ]; then\necho \"Missing Feedly Client ID\"\nFAILED=true\nfi\n\nif [ -z \"${FEEDLY_CLIENT_SECRET}\" ]; then\necho \"Missing Feedly Client Secret\"\nFAILED=true\nfi\n\nFEEDLY_CLIENT_SOURCE=\"${SRCROOT}/Feedly/OAuthAuthorizationClient+Feedly.swift\"\n\nif [ \"$FAILED\" = true ]; then\necho \"Missing Feedly client ID or secret. ${FEEDLY_CLIENT_SOURCE} not changed.\"\nexit 0\nfi\n\n# echo \"Substituting variables in: ${FEEDLY_CLIENT_SOURCE}\"\n\nif [ -e \"${FEEDLY_CLIENT_SOURCE}\" ]\nthen\n sed -i .tmp \"s|{FEEDLY_CLIENT_ID}|${FEEDLY_CLIENT_ID}|g; s|{FEEDLY_CLIENT_SECRET}|${FEEDLY_CLIENT_SECRET}|g\" $FEEDLY_CLIENT_SOURCE\n # echo \"`git diff ${FEEDLY_CLIENT_SOURCE}`\"\n rm -f \"${FEEDLY_CLIENT_SOURCE}.tmp\"\nelse\n echo \"File does not exist at ${FEEDLY_CLIENT_SOURCE}. Has it been moved or renamed?\"\n exit -1\nfi\n\necho \"All env values found!\"\n"; + }; + 9E964EBC2375517100A7AF2E /* Run Script: Reset OAuthAuthorizationClient+Feedly.swift */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Script: Reset OAuthAuthorizationClient+Feedly.swift"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "git checkout \"${SRCROOT}/Feedly/OAuthAuthorizationClient+Feedly.swift\"\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -932,6 +976,7 @@ 9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */, 9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */, 844B297D2106C7EC004020B3 /* Feed.swift in Sources */, + 9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */, 9E1D15572334355900F4944C /* FeedlyRequestStreamsOperation.swift in Sources */, 9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */, 9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */, @@ -972,6 +1017,7 @@ 9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */, 9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */, 9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */, + 9E964EB823754AC400A7AF2E /* OAuthAuthorizationClient+Feedly.swift in Sources */, 9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */, 84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */, 84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */, diff --git a/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift b/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift index 983c28e45..cd1a988d3 100644 --- a/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift +++ b/Frameworks/Account/AccountTests/Feedly/FeedlySyncAllOperationTests.swift @@ -130,7 +130,7 @@ class FeedlySyncAllOperationTests: XCTestCase { OperationQueue.main.addOperation(syncAll) - waitForExpectations(timeout: 2) + waitForExpectations(timeout: 5) } func performInitialSync() { diff --git a/Frameworks/Account/Credentials/CredentialsManager.swift b/Frameworks/Account/Credentials/CredentialsManager.swift index b52b6c30c..1043d9f6d 100644 --- a/Frameworks/Account/Credentials/CredentialsManager.swift +++ b/Frameworks/Account/Credentials/CredentialsManager.swift @@ -20,11 +20,12 @@ public struct CredentialsManager { }() public static func storeCredentials(_ credentials: Credentials, server: String) throws { - + var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, - kSecAttrAccount as String: credentials.username, - kSecAttrServer as String: server] - + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, + kSecAttrAccount as String: credentials.username, + kSecAttrServer as String: server] + if credentials.type != .basic { query[kSecAttrSecurityDomain as String] = credentials.type.rawValue } @@ -32,26 +33,25 @@ public struct CredentialsManager { if let securityGroup = keychainGroup { query[kSecAttrAccessGroup as String] = securityGroup } - + let secretData = credentials.secret.data(using: String.Encoding.utf8)! - let attributes: [String: Any] = [kSecValueData as String: secretData] - let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) - + query[kSecValueData as String] = secretData + + let status = SecItemAdd(query as CFDictionary, nil) + switch status { case errSecSuccess: return - case errSecItemNotFound: + case errSecDuplicateItem: break default: throw CredentialsError.unhandledError(status: status) } - guard status == errSecItemNotFound else { - return - } + var deleteQuery = query + deleteQuery.removeValue(forKey: kSecAttrAccessible as String) + SecItemDelete(deleteQuery as CFDictionary) - query[kSecValueData as String] = secretData - let addStatus = SecItemAdd(query as CFDictionary, nil) if addStatus != errSecSuccess { throw CredentialsError.unhandledError(status: status) diff --git a/Frameworks/Account/Feed.swift b/Frameworks/Account/Feed.swift index 0ca76adcd..cd3a17ec1 100644 --- a/Frameworks/Account/Feed.swift +++ b/Frameworks/Account/Feed.swift @@ -179,7 +179,8 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, De account.renameFeed(self, to: newName, completion: completion) } - // MARK: - PathIDUserInfoProvider + // MARK: - DeepLinkProvider + public var deepLinkUserInfo: [AnyHashable : Any] { return [ DeepLinkKey.accountID.rawValue: account?.accountID ?? "", diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index 50eb9356c..ea3027a4f 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -78,7 +78,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) refreshProgress.addToNumberOfTasksAndRemaining(5) @@ -112,7 +111,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - retrieveCredentialsIfNecessary(account) os_log(.debug, log: log, "Sending article statuses...") @@ -169,7 +167,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { - retrieveCredentialsIfNecessary(account) os_log(.debug, log: log, "Refreshing article statuses...") @@ -216,7 +213,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) var fileData: Data? @@ -263,7 +259,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func addFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) if let folder = account.ensureFolder(with: name) { completion(.success(folder)) } else { @@ -272,7 +267,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) guard folder.hasAtLeastOneFeed() else { folder.name = name @@ -300,7 +294,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) // Feedbin uses tags and if at least one feed isn't tagged, then the folder doesn't exist on their system guard folder.hasAtLeastOneFeed() else { @@ -364,7 +357,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) refreshProgress.addToNumberOfTasksAndRemaining(1) caller.createSubscription(url: url) { result in @@ -397,7 +389,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) // This error should never happen guard let subscriptionID = feed.subscriptionID else { @@ -433,7 +424,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) if from is Account { addFeed(for: account, with: feed, to: to, completion: completion) } else { @@ -449,7 +439,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) if let folder = container as? Folder, let feedID = Int(feed.feedID) { refreshProgress.addToNumberOfTasksAndRemaining(1) @@ -482,7 +471,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) if let existingFeed = account.existingFeed(withURL: feed.url) { account.addFeed(existingFeed, to: container) { result in @@ -507,7 +495,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) let group = DispatchGroup() @@ -536,7 +523,6 @@ final class FeedbinAccountDelegate: AccountDelegate { } func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set
? { - retrieveCredentialsIfNecessary(account) let syncStatuses = articles.map { article in return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag) @@ -1308,11 +1294,5 @@ private extension FeedbinAccountDelegate { } } - - func retrieveCredentialsIfNecessary(_ account: Account) { - if credentials == nil { - credentials = try? account.retrieveCredentials(type: .basic) - } - } } diff --git a/Frameworks/Account/Feedly/FeedlyAPICaller.swift b/Frameworks/Account/Feedly/FeedlyAPICaller.swift index dfe4e9c99..38fd68734 100644 --- a/Frameworks/Account/Feedly/FeedlyAPICaller.swift +++ b/Frameworks/Account/Feedly/FeedlyAPICaller.swift @@ -31,20 +31,10 @@ final class FeedlyAPICaller { var oauthAuthorizationClient: OAuthAuthorizationClient { switch self { - case .cloud: - /// Models private NetNewsWire client secrets. - /// https://developer.feedly.com/v3/auth/#authenticating-a-user-and-obtaining-an-auth-code - return OAuthAuthorizationClient(id: "{FEEDLY-ID}", - redirectUri: "{FEEDLY-REDIRECT-URI}", - state: nil, - secret: "{FEEDLY-SECRET}") case .sandbox: - /// Models public sandbox API values found at: - /// https://groups.google.com/forum/#!topic/feedly-cloud/WwQWMgDmOuw - return OAuthAuthorizationClient(id: "sandbox", - redirectUri: "http://localhost", - state: nil, - secret: "ReVGXA6WekanCxbf") + return .feedlySandboxClient + case .cloud: + return .feedlyCloudClient } } } diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate+OAuth.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate+OAuth.swift index abe0c465a..f0c3bae48 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate+OAuth.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate+OAuth.swift @@ -27,11 +27,8 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting { private static let oauthAuthorizationGrantScope = "https://cloud.feedly.com/subscriptions" - static var oauthAuthorizationClient: OAuthAuthorizationClient { - return environment.oauthAuthorizationClient - } - - static func oauthAuthorizationCodeGrantRequest(for client: OAuthAuthorizationClient) -> URLRequest { + static func oauthAuthorizationCodeGrantRequest() -> URLRequest { + let client = environment.oauthAuthorizationClient let authorizationRequest = OAuthAuthorizationRequest(clientId: client.id, redirectUri: client.redirectUri, scope: oauthAuthorizationGrantScope, @@ -40,7 +37,8 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting { return FeedlyAPICaller.authorizationCodeUrlRequest(for: authorizationRequest, baseUrlComponents: baseURLComponents) } - static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, client: OAuthAuthorizationClient, transport: Transport, completionHandler: @escaping (Result) -> ()) { + static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completionHandler: @escaping (Result) -> ()) { + let client = environment.oauthAuthorizationClient let request = OAuthAccessTokenRequest(authorizationResponse: response, scope: oauthAuthorizationGrantScope, client: client) diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 37e33ff16..fbec23b02 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -17,17 +17,10 @@ final class FeedlyAccountDelegate: AccountDelegate { /// Feedly has a sandbox API and a production API. /// This property is referred to when clients need to know which environment it should be pointing to. + /// The value of this proptery must match any `OAuthAuthorizationClient` used. + /// Currently this is always returning the cloud API, but we are leaving it stubbed out for now. static var environment: FeedlyAPICaller.API { - #if DEBUG - // https://developer.feedly.com/v3/developer/ - if let token = ProcessInfo.processInfo.environment["FEEDLY_DEV_ACCESS_TOKEN"], !token.isEmpty { - return .cloud - } - return .sandbox - - #else return .cloud - #endif } // TODO: Kiel, if you decide not to support OPML import you will have to disallow it in the behaviors @@ -53,6 +46,8 @@ final class FeedlyAccountDelegate: AccountDelegate { } } + let oauthAuthorizationClient: OAuthAuthorizationClient + var accountMetadata: AccountMetadata? var refreshProgress = DownloadProgress(numberOfTasks: 0) @@ -97,6 +92,7 @@ final class FeedlyAccountDelegate: AccountDelegate { let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3") self.database = SyncDatabase(databaseFilePath: databaseFilePath) + self.oauthAuthorizationClient = api.oauthAuthorizationClient } // MARK: Account API @@ -128,7 +124,9 @@ final class FeedlyAccountDelegate: AccountDelegate { let date = Date() operation.syncCompletionHandler = { [weak self] result in - self?.accountMetadata?.lastArticleFetch = date + if case .success = result { + self?.accountMetadata?.lastArticleFetch = date + } os_log(.debug, log: log, "Sync took %{public}.3f seconds", -date.timeIntervalSinceNow) progress.completeTask() @@ -484,8 +482,7 @@ final class FeedlyAccountDelegate: AccountDelegate { func accountDidInitialize(_ account: Account) { credentials = try? account.retrieveCredentials(type: .oauthAccessToken) - let client = FeedlyAccountDelegate.oauthAuthorizationClient - let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: client, log: log) + let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: oauthAuthorizationClient, log: log) operationQueue.addOperation(refreshAccessToken) } diff --git a/Frameworks/Account/Feedly/OAuthAccountAuthorizationOperation.swift b/Frameworks/Account/Feedly/OAuthAccountAuthorizationOperation.swift new file mode 100644 index 000000000..bc73c6f3e --- /dev/null +++ b/Frameworks/Account/Feedly/OAuthAccountAuthorizationOperation.swift @@ -0,0 +1,187 @@ +// +// OAuthAccountAuthorizationOperation.swift +// NetNewsWire +// +// Created by Kiel Gillard on 8/11/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation +import AuthenticationServices + +public protocol OAuthAccountAuthorizationOperationDelegate: class { + func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) + func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) +} + +public final class OAuthAccountAuthorizationOperation: Operation, ASWebAuthenticationPresentationContextProviding { + + public weak var presentationAnchor: ASPresentationAnchor? + public weak var delegate: OAuthAccountAuthorizationOperationDelegate? + + private let accountType: AccountType + private let oauthClient: OAuthAuthorizationClient + private var session: ASWebAuthenticationSession? + + public init(accountType: AccountType) { + self.accountType = accountType + self.oauthClient = Account.oauthAuthorizationClient(for: accountType) + } + + override public func main() { + assert(Thread.isMainThread) + assert(presentationAnchor != nil, "\(self) outlived presentation anchor.") + + guard !isCancelled else { + didFinish() + return + } + + let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType) + + guard let url = request.url else { + return DispatchQueue.main.async { + self.didEndAuthentication(url: nil, error: URLError(.badURL)) + } + } + + guard let redirectUri = URL(string: oauthClient.redirectUri), let scheme = redirectUri.scheme else { + assertionFailure("Could not get callback URL scheme from \(oauthClient.redirectUri)") + return DispatchQueue.main.async { + self.didEndAuthentication(url: nil, error: URLError(.badURL)) + } + } + + let session = ASWebAuthenticationSession(url: url, callbackURLScheme: scheme) { url, error in + DispatchQueue.main.async { [weak self] in + self?.didEndAuthentication(url: url, error: error) + } + } + self.session = session + session.presentationContextProvider = self + + session.start() + } + + override public func cancel() { + session?.cancel() + super.cancel() + } + + private func didEndAuthentication(url: URL?, error: Error?) { + guard !isCancelled else { + didFinish() + return + } + + do { + guard let url = url else { + if let error = error { + throw error + } + throw URLError(.badURL) + } + + let response = try OAuthAuthorizationResponse(url: url, client: oauthClient) + + Account.requestOAuthAccessToken(with: response, client: oauthClient, accountType: accountType, completionHandler: didEndRequestingAccessToken(_:)) + + } catch is ASWebAuthenticationSessionError { + didFinish() // Primarily, cancellation. + + } catch { + didFinish(error) + } + } + + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + guard let anchor = presentationAnchor else { + fatalError("\(self) has outlived presentation anchor.") + } + return anchor + } + + private func didEndRequestingAccessToken(_ result: Result) { + guard !isCancelled else { + didFinish() + return + } + + switch result { + case .success(let tokenResponse): + saveAccount(for: tokenResponse) + case .failure(let error): + didFinish(error) + } + } + + private func saveAccount(for grant: OAuthAuthorizationGrant) { + // TODO: Find an already existing account for this username? + let account = AccountManager.shared.createAccount(type: .feedly) + do { + + // Store the refresh token first because it sends this token to the account delegate. + if let token = grant.refreshToken { + try account.storeCredentials(token) + } + + // Now store the access token because we want the account delegate to use it. + try account.storeCredentials(grant.accessToken) + + delegate?.oauthAccountAuthorizationOperation(self, didCreate: account) + + didFinish() + } catch { + didFinish(error) + } + } + + // MARK: Managing Operation State + + private func didFinish() { + assert(Thread.isMainThread) + assert(!isFinished, "Finished operation is attempting to finish again.") + self.isExecutingOperation = false + self.isFinishedOperation = true + } + + private func didFinish(_ error: Error) { + assert(Thread.isMainThread) + assert(!isFinished, "Finished operation is attempting to finish again.") + delegate?.oauthAccountAuthorizationOperation(self, didFailWith: error) + didFinish() + } + + override public func start() { + isExecutingOperation = true + DispatchQueue.main.async { + self.main() + } + } + + override public var isExecuting: Bool { + return isExecutingOperation + } + + private var isExecutingOperation = false { + willSet { + willChangeValue(for: \.isExecuting) + } + didSet { + didChangeValue(for: \.isExecuting) + } + } + + override public var isFinished: Bool { + return isFinishedOperation + } + + private var isFinishedOperation = false { + willSet { + willChangeValue(for: \.isFinished) + } + didSet { + didChangeValue(for: \.isFinished) + } + } +} diff --git a/Frameworks/Account/Feedly/OAuthAuthorizationClient+Feedly.swift b/Frameworks/Account/Feedly/OAuthAuthorizationClient+Feedly.swift new file mode 100644 index 000000000..c114bae87 --- /dev/null +++ b/Frameworks/Account/Feedly/OAuthAuthorizationClient+Feedly.swift @@ -0,0 +1,34 @@ +// +// OAuthAuthorizationClient+NetNewsWire.swift +// Account +// +// Created by Kiel Gillard on 8/11/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation + +extension OAuthAuthorizationClient { + + static var feedlyCloudClient: OAuthAuthorizationClient { + /// Models private NetNewsWire client secrets. + /// These placeholders are substitued at build time using a Run Script phase with build settings. + /// https://developer.feedly.com/v3/auth/#authenticating-a-user-and-obtaining-an-auth-code + return OAuthAuthorizationClient(id: "{FEEDLY_CLIENT_ID}", + redirectUri: "netnewswire://auth/feedly", + state: nil, + secret: "{FEEDLY_CLIENT_SECRET}") + } + + static var feedlySandboxClient: OAuthAuthorizationClient { + /// We use this funky redirect URI because ASWebAuthenticationSession will try to load http://localhost URLs. + /// See https://developer.feedly.com/v3/sandbox/ for more information. + /// The return value models public sandbox API values found at: + /// https://groups.google.com/forum/#!topic/feedly-cloud/WwQWMgDmOuw + /// They are due to expire on November 30 2019. + return OAuthAuthorizationClient(id: "sandbox", + redirectUri: "urn:ietf:wg:oauth:2.0:oob", + state: nil, + secret: "ReVGXA6WekanCxbf") + } +} diff --git a/Frameworks/Account/Feedly/OAuthAuthorizationCodeGranting.swift b/Frameworks/Account/Feedly/OAuthAuthorizationCodeGranting.swift index 433687c32..3f1aabbaf 100644 --- a/Frameworks/Account/Feedly/OAuthAuthorizationCodeGranting.swift +++ b/Frameworks/Account/Feedly/OAuthAuthorizationCodeGranting.swift @@ -61,7 +61,7 @@ public struct OAuthAuthorizationResponse { public extension OAuthAuthorizationResponse { init(url: URL, client: OAuthAuthorizationClient) throws { - guard let host = url.host, client.redirectUri.contains(host) else { + guard let scheme = url.scheme, client.redirectUri.hasPrefix(scheme) else { throw URLError(.unsupportedURL) } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { @@ -165,10 +165,8 @@ public protocol OAuthAuthorizationCodeGrantRequesting { } protocol OAuthAuthorizationGranting: AccountDelegate { + + static func oauthAuthorizationCodeGrantRequest() -> URLRequest - static var oauthAuthorizationClient: OAuthAuthorizationClient { get } - - static func oauthAuthorizationCodeGrantRequest(for client: OAuthAuthorizationClient) -> URLRequest - - static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, client: OAuthAuthorizationClient, transport: Transport, completionHandler: @escaping (Result) -> ()) + static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completionHandler: @escaping (Result) -> ()) } diff --git a/Frameworks/Account/Folder.swift b/Frameworks/Account/Folder.swift index c341e110a..e23e7804a 100644 --- a/Frameworks/Account/Folder.swift +++ b/Frameworks/Account/Folder.swift @@ -33,7 +33,8 @@ public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCoun return name ?? Folder.untitledName } - // MARK: - PathIDUserInfoProvider + // MARK: - DeepLinkProvider + public var deepLinkUserInfo: [AnyHashable : Any] { return [ DeepLinkKey.accountID.rawValue: account?.accountID ?? "", diff --git a/Frameworks/Account/xcconfig/Account_project.xcconfig b/Frameworks/Account/xcconfig/Account_project.xcconfig index f1144a6e9..8e096dabd 100644 --- a/Frameworks/Account/xcconfig/Account_project.xcconfig +++ b/Frameworks/Account/xcconfig/Account_project.xcconfig @@ -7,9 +7,10 @@ PROVISIONING_PROFILE_SPECIFIER = // DeveloperSettings.xcconfig is #included here #include? "../../../SharedXcodeSettings/DeveloperSettings.xcconfig" +#include? "../../../SharedXcodeSettings/ProjectSettings.xcconfig" SDKROOT = macosx -MACOSX_DEPLOYMENT_TARGET = 10.14 +MACOSX_DEPLOYMENT_TARGET = 10.15 IPHONEOS_DEPLOYMENT_TARGET = 13.0 SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator @@ -18,7 +19,6 @@ SWIFT_VERSION = 5.1 COMBINE_HIDPI_IMAGES = YES COPY_PHASE_STRIP = NO -MACOSX_DEPLOYMENT_TARGET = 10.14 ALWAYS_SEARCH_USER_PATHS = NO CURRENT_PROJECT_VERSION = 1 VERSION_INFO_PREFIX = diff --git a/Mac/AppAssets.swift b/Mac/AppAssets.swift index ab5eb8dc6..644ce65ea 100644 --- a/Mac/AppAssets.swift +++ b/Mac/AppAssets.swift @@ -16,12 +16,6 @@ extension NSImage.Name { struct AppAssets { - static var genericFeedImage: IconImage? = { - let path = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/BookmarkIcon.icns" - let image = RSImage(contentsOfFile: path) - return image != nil ? IconImage(image!) : nil - }() - static var timelineStar: RSImage! = { return RSImage(named: .timelineStar) }() @@ -50,6 +44,14 @@ struct AppAssets { return RSImage(named: "articleExtractorError") }() + static var articleExtractorInactiveDark: RSImage! = { + return RSImage(named: "articleExtractorInactiveDark") + }() + + static var articleExtractorInactiveLight: RSImage! = { + return RSImage(named: "articleExtractorInactiveLight") + }() + static var articleExtractorProgress1: RSImage! = { return RSImage(named: "articleExtractorProgress1") }() diff --git a/Mac/Inspector/FeedInspectorViewController.swift b/Mac/Inspector/FeedInspectorViewController.swift index 0c2bbd568..964668798 100644 --- a/Mac/Inspector/FeedInspectorViewController.swift +++ b/Mac/Inspector/FeedInspectorViewController.swift @@ -12,7 +12,7 @@ import Account final class FeedInspectorViewController: NSViewController, Inspector { - @IBOutlet weak var imageView: NSImageView? + @IBOutlet weak var iconView: IconView! @IBOutlet weak var nameTextField: NSTextField? @IBOutlet weak var homePageURLTextField: NSTextField? @IBOutlet weak var urlTextField: NSTextField? @@ -43,11 +43,7 @@ final class FeedInspectorViewController: NSViewController, Inspector { // MARK: NSViewController override func viewDidLoad() { - imageView!.wantsLayer = true - imageView!.layer?.cornerRadius = 4.0 - updateUI() - NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil) } @@ -101,25 +97,21 @@ private extension FeedInspectorViewController { } func updateImage() { - guard let feed = feed else { - imageView?.image = nil + guard let feed = feed, let iconView = iconView else { return } if let feedIcon = appDelegate.feedIconDownloader.icon(for: feed) { - imageView?.image = feedIcon.image + iconView.iconImage = feedIcon return } - if let favicon = appDelegate.faviconDownloader.favicon(for: feed)?.image { - if favicon.size.height < 16.0 && favicon.size.width < 16.0 { - favicon.size = NSSize(width: 16, height: 16) - } - imageView?.image = favicon + if let favicon = appDelegate.faviconDownloader.favicon(for: feed) { + iconView.iconImage = favicon return } - imageView?.image = AppAssets.genericFeedImage?.image + iconView.iconImage = feed.smallIcon } func updateName() { diff --git a/Mac/Inspector/Inspector.storyboard b/Mac/Inspector/Inspector.storyboard index eb460de87..b35d220cf 100644 --- a/Mac/Inspector/Inspector.storyboard +++ b/Mac/Inspector/Inspector.storyboard @@ -1,8 +1,7 @@ - + - - + @@ -37,14 +36,6 @@ - - - - - - - - @@ -114,13 +105,19 @@ Field + + + + + + + - - + @@ -128,11 +125,12 @@ Field + - + @@ -142,7 +140,7 @@ Field - + @@ -151,7 +149,7 @@ Field - + @@ -294,7 +292,6 @@ Field - diff --git a/Mac/MainWindow/ArticleExtractorButton.swift b/Mac/MainWindow/ArticleExtractorButton.swift index 69e250ed5..67b9f75da 100644 --- a/Mac/MainWindow/ArticleExtractorButton.swift +++ b/Mac/MainWindow/ArticleExtractorButton.swift @@ -59,7 +59,15 @@ class ArticleExtractorButton: NSButton { case isInProgress: addAnimatedSublayer(to: hostedLayer) default: - addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractor, opacity: opacity) + if NSApplication.shared.isActive { + addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractor, opacity: opacity) + } else { + if NSApplication.shared.effectiveAppearance.isDarkMode { + addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractorInactiveDark, opacity: opacity) + } else { + addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractorInactiveLight, opacity: opacity) + } + } } } diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index 4ebf7a697..82dc20524 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -39,6 +39,7 @@ final class DetailWebViewController: NSViewController, WKUIDelegate { } #endif + private let articleIconSchemeHandler = ArticleIconSchemeHandler() private var waitingForFirstReload = false private let keyboardDelegate = DetailKeyboardDelegate() @@ -65,6 +66,7 @@ final class DetailWebViewController: NSViewController, WKUIDelegate { let configuration = WKWebViewConfiguration() configuration.preferences = preferences + configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) let userContentController = WKUserContentController() userContentController.add(self, name: MessageName.mouseDidEnter) @@ -185,8 +187,10 @@ private extension DetailWebViewController { case .loading: rendering = ArticleRenderer.loadingHTML(style: style) case .article(let article): + articleIconSchemeHandler.currentArticle = article rendering = ArticleRenderer.articleHTML(article: article, style: style) case .extracted(let article, let extractedArticle): + articleIconSchemeHandler.currentArticle = article rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style) } diff --git a/Mac/MainWindow/Timeline/Cell/TimelineIconView.swift b/Mac/MainWindow/IconView.swift similarity index 94% rename from Mac/MainWindow/Timeline/Cell/TimelineIconView.swift rename to Mac/MainWindow/IconView.swift index f537c3124..e03ea6be4 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineIconView.swift +++ b/Mac/MainWindow/IconView.swift @@ -8,7 +8,7 @@ import AppKit -final class TimelineIconView: NSView { +final class IconView: NSView { var iconImage: IconImage? = nil { didSet { @@ -71,17 +71,18 @@ final class TimelineIconView: NSView { return } - let color = NSApplication.shared.effectiveAppearance.isDarkMode ? TimelineIconView.darkBackgroundColor : TimelineIconView.lightBackgroundColor + let color = NSApplication.shared.effectiveAppearance.isDarkMode ? IconView.darkBackgroundColor : IconView.lightBackgroundColor color.set() dirtyRect.fill() } } -private extension TimelineIconView { +private extension IconView { func commonInit() { addSubview(imageView) wantsLayer = true + layer?.cornerRadius = 4.0 } func rectForImageView() -> NSRect { diff --git a/Mac/MainWindow/SharingServicePickerDelegate.swift b/Mac/MainWindow/SharingServicePickerDelegate.swift index 5e2d38dc0..45cbd9eb7 100644 --- a/Mac/MainWindow/SharingServicePickerDelegate.swift +++ b/Mac/MainWindow/SharingServicePickerDelegate.swift @@ -41,7 +41,7 @@ import RSCore return nil } - let image = sendToCommand.image ?? AppAssets.genericFeedImage?.image ?? NSImage() + let image = sendToCommand.image ?? NSImage() return NSSharingService(title: sendToCommand.title, image: image, alternateImage: nil) { sendToCommand.sendObject(object, selectedText: nil) } diff --git a/Mac/MainWindow/Sidebar/Cell/SidebarCell.swift b/Mac/MainWindow/Sidebar/Cell/SidebarCell.swift index 610a51ace..7b021a073 100644 --- a/Mac/MainWindow/Sidebar/Cell/SidebarCell.swift +++ b/Mac/MainWindow/Sidebar/Cell/SidebarCell.swift @@ -81,8 +81,7 @@ class SidebarCell : NSTableCellView { }() private let faviconImageView: NSImageView = { - let iconImage = AppAssets.genericFeedImage - let imageView = iconImage != nil ? NSImageView(image: iconImage!.image) : NSImageView(frame: NSRect.zero) + let imageView = NSImageView(frame: NSRect.zero) imageView.animates = false imageView.imageAlignment = .alignCenter imageView.imageScaling = .scaleProportionallyDown diff --git a/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift b/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift index 219e7bdc5..2271a7a53 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift +++ b/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift @@ -18,7 +18,7 @@ class TimelineTableCellView: NSTableCellView { private let dateView = TimelineTableCellView.singleLineTextField() private let feedNameView = TimelineTableCellView.singleLineTextField() - private lazy var iconView = TimelineIconView() + private lazy var iconView = IconView() private let starView = TimelineTableCellView.imageView(with: AppAssets.timelineStar, scaling: .scaleNone) private let separatorView = TimelineTableCellView.separatorView() diff --git a/Mac/Preferences/Accounts/AccountsAddViewController.swift b/Mac/Preferences/Accounts/AccountsAddViewController.swift index 02b87bd38..db0def90c 100644 --- a/Mac/Preferences/Accounts/AccountsAddViewController.swift +++ b/Mac/Preferences/Accounts/AccountsAddViewController.swift @@ -101,9 +101,10 @@ extension AccountsAddViewController: NSTableViewDelegate { accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!) accountsAddWindowController = accountsReaderAPIWindowController case .feedly: - let accountsFeedlyWindowController = AccountsFeedlyWebWindowController() - accountsFeedlyWindowController.runSheetOnWindow(self.view.window!) - accountsAddWindowController = accountsFeedlyWindowController + let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly) + addAccount.delegate = self + addAccount.presentationAnchor = self.view.window! + OperationQueue.main.addOperation(addAccount) default: break } @@ -113,3 +114,23 @@ extension AccountsAddViewController: NSTableViewDelegate { } } + +// MARK: OAuthAccountAuthorizationOperationDelegate + +extension AccountsAddViewController: OAuthAccountAuthorizationOperationDelegate { + + func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) { + account.refreshAll { [weak self] result in + switch result { + case .success: + break + case .failure(let error): + self?.presentError(error) + } + } + } + + func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) { + view.window?.presentError(error) + } +} diff --git a/Mac/Preferences/Accounts/AccountsFeedlyWeb.xib b/Mac/Preferences/Accounts/AccountsFeedlyWeb.xib deleted file mode 100644 index 73455ac32..000000000 --- a/Mac/Preferences/Accounts/AccountsFeedlyWeb.xib +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Mac/Preferences/Accounts/AccountsFeedlyWebWindowController.swift b/Mac/Preferences/Accounts/AccountsFeedlyWebWindowController.swift deleted file mode 100644 index 7529a8a9a..000000000 --- a/Mac/Preferences/Accounts/AccountsFeedlyWebWindowController.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// AccountsFeedlyWebWindowController.swift -// NetNewsWire -// -// Created by Kiel Gillard on 30/8/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import Cocoa -import Account -import WebKit - -class AccountsFeedlyWebWindowController: NSWindowController, WKNavigationDelegate { - - @IBOutlet private weak var webView: WKWebView! - - private weak var hostWindow: NSWindow? - - convenience init() { - self.init(windowNibName: NSNib.Name("AccountsFeedlyWeb")) - } - - // MARK: API - - func runSheetOnWindow(_ hostWindow: NSWindow, completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil) { - self.hostWindow = hostWindow - hostWindow.beginSheet(window!, completionHandler: handler) - beginAuthorization() - } - - // MARK: Requesting an Access Token - let client = Account.oauthAuthorizationClient(for: .feedly) - - private func beginAuthorization() { - let request = Account.oauthAuthorizationCodeGrantRequest(for: .feedly, client: client) - webView.load(request) - } - - private func requestAccessToken(for response: OAuthAuthorizationResponse) { - Account.requestOAuthAccessToken(with: response, client: client, accountType: .feedly) { [weak self] result in - switch result { - case .success(let tokenResponse): - self?.saveAccount(for: tokenResponse) - case .failure(let error): - NSApplication.shared.presentError(error) - } - } - } - - // MARK: Actions - - @IBAction func cancel(_ sender: Any) { - hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) - } - - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - - do { - guard let url = navigationAction.request.url else { return } - - let response = try OAuthAuthorizationResponse(url: url, client: client) - - requestAccessToken(for: response) - - // No point the web view trying to load this. - return decisionHandler(.cancel) - - } catch let error as OAuthAuthorizationErrorResponse { - NSApplication.shared.presentError(error) - - } catch { - print(error) - } - - decisionHandler(.allow) - } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - print(error) - } - - private func saveAccount(for grant: OAuthAuthorizationGrant) { - // TODO: Find an already existing account for this username? - let account = AccountManager.shared.createAccount(type: .feedly) - do { - - // Store the refresh token first because it sends this token to the account delegate. - if let token = grant.refreshToken { - try account.storeCredentials(token) - } - - // Now store the access token because we want the account delegate to use it. - try account.storeCredentials(grant.accessToken) - - self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) - } catch { - NSApplication.shared.presentError(error) - } - } -} diff --git a/Mac/Resources/Assets.xcassets/articleExtractorInactiveDark.imageset/ArticleExtractorInactiveDark.pdf b/Mac/Resources/Assets.xcassets/articleExtractorInactiveDark.imageset/ArticleExtractorInactiveDark.pdf new file mode 100644 index 000000000..28edebd24 Binary files /dev/null and b/Mac/Resources/Assets.xcassets/articleExtractorInactiveDark.imageset/ArticleExtractorInactiveDark.pdf differ diff --git a/Mac/Resources/Assets.xcassets/articleExtractorInactiveDark.imageset/Contents.json b/Mac/Resources/Assets.xcassets/articleExtractorInactiveDark.imageset/Contents.json new file mode 100644 index 000000000..3aa79833f --- /dev/null +++ b/Mac/Resources/Assets.xcassets/articleExtractorInactiveDark.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ArticleExtractorInactiveDark.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Mac/Resources/Assets.xcassets/articleExtractorInactiveLight.imageset/ArticleExtractorInactiveLight.pdf b/Mac/Resources/Assets.xcassets/articleExtractorInactiveLight.imageset/ArticleExtractorInactiveLight.pdf new file mode 100644 index 000000000..d09538c0c Binary files /dev/null and b/Mac/Resources/Assets.xcassets/articleExtractorInactiveLight.imageset/ArticleExtractorInactiveLight.pdf differ diff --git a/Mac/Resources/Assets.xcassets/articleExtractorInactiveLight.imageset/Contents.json b/Mac/Resources/Assets.xcassets/articleExtractorInactiveLight.imageset/Contents.json new file mode 100644 index 000000000..1dff9cf00 --- /dev/null +++ b/Mac/Resources/Assets.xcassets/articleExtractorInactiveLight.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ArticleExtractorInactiveLight.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 94820373d..819da2fc1 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -108,19 +108,18 @@ 5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; }; 5183CCE6226F4E110010922C /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; }; 5183CCE8226F68D90010922C /* AccountRefreshTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */; }; - 5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */; }; 518651B223555EB20078E021 /* NNW3Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518651AB23555EB20078E021 /* NNW3Document.swift */; }; 518651DA235621840078E021 /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518651D9235621840078E021 /* ImageTransition.swift */; }; 5186A635235EF3A800C97195 /* VibrantLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5186A634235EF3A800C97195 /* VibrantLabel.swift */; }; 518B2EE82351B45600400001 /* NetNewsWire_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D61952029031D009BC708 /* NetNewsWire_iOSTests.swift */; }; + 518C3193237B00D9004D740F /* ArticleIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */; }; + 518C3194237B00DA004D740F /* ArticleIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */; }; 51934CCB230F599B006127BE /* ThemedNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CC1230F5963006127BE /* ThemedNavigationController.swift */; }; 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; }; 51938DF2231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; }; 51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; }; 519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; }; - 519D740623243CC0008BB345 /* RefreshInterval-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519D740523243CC0008BB345 /* RefreshInterval-Extensions.swift */; }; 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E743422C663F900A78E47 /* SceneDelegate.swift */; }; - 51A16997235E10D700EB091F /* RefreshIntervalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1698D235E10D600EB091F /* RefreshIntervalViewController.swift */; }; 51A16999235E10D700EB091F /* LocalAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1698F235E10D600EB091F /* LocalAccountViewController.swift */; }; 51A1699A235E10D700EB091F /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51A16990235E10D600EB091F /* Settings.storyboard */; }; 51A1699B235E10D700EB091F /* AccountInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16991235E10D600EB091F /* AccountInspectorViewController.swift */; }; @@ -128,7 +127,6 @@ 51A1699D235E10D700EB091F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16993235E10D600EB091F /* SettingsViewController.swift */; }; 51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16995235E10D600EB091F /* AboutViewController.swift */; }; 51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */; }; - 51AF460E232488C6001742EF /* Account-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AF460D232488C6001742EF /* Account-Extensions.swift */; }; 51B62E68233186730085F949 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B62E67233186730085F949 /* IconView.swift */; }; 51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; }; 51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; }; @@ -257,7 +255,7 @@ 6581C74220CED60100F4AD34 /* ToolbarItemIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */; }; 65ED3FB7235DEF6C0081F399 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; 65ED3FB8235DEF6C0081F399 /* CrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848B937121C8C5540038DC0D /* CrashReporter.swift */; }; - 65ED3FB9235DEF6C0081F399 /* TimelineIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* TimelineIconView.swift */; }; + 65ED3FB9235DEF6C0081F399 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* IconView.swift */; }; 65ED3FBA235DEF6C0081F399 /* ArticleExtractorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A92332C2FD0090D516 /* ArticleExtractorConfig.swift */; }; 65ED3FBB235DEF6C0081F399 /* InspectorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BBB12C20142A4700F054F5 /* InspectorWindowController.swift */; }; 65ED3FBC235DEF6C0081F399 /* ColorHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F78227716380050506E /* ColorHash.swift */; }; @@ -276,7 +274,6 @@ 65ED3FC9235DEF6C0081F399 /* SmartFeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */; }; 65ED3FCA235DEF6C0081F399 /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; }; 65ED3FCB235DEF6C0081F399 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97621ED9EB96007D329B /* SidebarViewController.swift */; }; - 65ED3FCC235DEF6C0081F399 /* AccountsFeedlyWebWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */; }; 65ED3FCD235DEF6C0081F399 /* SidebarOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97601ED9EB96007D329B /* SidebarOutlineView.swift */; }; 65ED3FCE235DEF6C0081F399 /* DetailKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */; }; 65ED3FCF235DEF6C0081F399 /* TimelineContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405DD9822153B6B008CE1BF /* TimelineContainerView.swift */; }; @@ -307,7 +304,6 @@ 65ED3FE8235DEF6C0081F399 /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; }; 65ED3FE9235DEF6C0081F399 /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; }; 65ED3FEA235DEF6C0081F399 /* SidebarViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */; }; - 65ED3FEB235DEF6C0081F399 /* (null) in Sources */ = {isa = PBXBuildFile; }; 65ED3FEC235DEF6C0081F399 /* RSHTMLMetadata+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611A11FCB769D0086A189 /* RSHTMLMetadata+Extension.swift */; }; 65ED3FED235DEF6C0081F399 /* SendToMarsEditCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */; }; 65ED3FEE235DEF6C0081F399 /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; }; @@ -407,7 +403,6 @@ 65ED4050235DEF6C0081F399 /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; }; 65ED4051235DEF6C0081F399 /* TimelineKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */; }; 65ED4052235DEF6C0081F399 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; }; - 65ED4053235DEF6C0081F399 /* AccountsFeedlyWeb.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */; }; 65ED4054235DEF6C0081F399 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 848363062262A3DD00DA1D35 /* Main.storyboard */; }; 65ED4055235DEF6C0081F399 /* AccountsAdd.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51EF0F8D2279C9260050506E /* AccountsAdd.xib */; }; 65ED4056235DEF6C0081F399 /* NetNewsWire.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FC8A22629E8F00D921D6 /* NetNewsWire.sdef */; }; @@ -494,7 +489,7 @@ 84702AA41FA27AC0006B8943 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8472058020142E8900AD578B /* FeedInspectorViewController.swift */; }; 8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; }; - 847CD6CA232F4CBF00FAC46D /* TimelineIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* TimelineIconView.swift */; }; + 847CD6CA232F4CBF00FAC46D /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* IconView.swift */; }; 847E64A02262783000E00365 /* NSAppleEventDescriptor+UserRecordFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */; }; 848362FD2262A30800DA1D35 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 848362FC2262A30800DA1D35 /* styleSheet.css */; }; 848362FF2262A30E00DA1D35 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; }; @@ -615,8 +610,6 @@ 84F9EAF4213660A100CF2DE4 /* testGenericScript.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE1213660A100CF2DE4 /* testGenericScript.applescript */; }; 84F9EAF5213660A100CF2DE4 /* establishMainWindowStartingState.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */; }; 84FF69B11FC3793300DC198E /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; }; - 9EA33BB92318F8C10097B644 /* AccountsFeedlyWebWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */; }; - 9EA33BBA2318F8C10097B644 /* AccountsFeedlyWeb.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */; }; B528F81E23333C7E00E735DD /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = B528F81D23333C7E00E735DD /* page.html */; }; D553738B20186C20006D8857 /* Article+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D553737C20186C1F006D8857 /* Article+Scriptability.swift */; }; D57BE6E0204CD35F00D11AAC /* NSScriptCommand+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57BE6DF204CD35F00D11AAC /* NSScriptCommand+NetNewsWire.swift */; }; @@ -1284,9 +1277,7 @@ 51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = ""; }; 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = ""; }; 519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = ""; }; - 519D740523243CC0008BB345 /* RefreshInterval-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RefreshInterval-Extensions.swift"; sourceTree = ""; }; 519E743422C663F900A78E47 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 51A1698D235E10D600EB091F /* RefreshIntervalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshIntervalViewController.swift; sourceTree = ""; }; 51A1698F235E10D600EB091F /* LocalAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalAccountViewController.swift; sourceTree = ""; }; 51A16990235E10D600EB091F /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = ""; }; 51A16991235E10D600EB091F /* AccountInspectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountInspectorViewController.swift; sourceTree = ""; }; @@ -1294,7 +1285,6 @@ 51A16993235E10D600EB091F /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 51A16995235E10D600EB091F /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = ""; }; - 51AF460D232488C6001742EF /* Account-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account-Extensions.swift"; sourceTree = ""; }; 51B62E67233186730085F949 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = ""; }; 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleActivityItemSource.swift; sourceTree = ""; }; 51BB7C302335ACDE008E8144 /* page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = page.html; sourceTree = ""; }; @@ -1416,7 +1406,7 @@ 8472058020142E8900AD578B /* FeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInspectorViewController.swift; sourceTree = ""; }; 847752FE2008879500D93690 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedDelegate.swift; sourceTree = ""; }; - 847CD6C9232F4CBF00FAC46D /* TimelineIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineIconView.swift; sourceTree = ""; }; + 847CD6C9232F4CBF00FAC46D /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = ""; }; 847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAppleEventDescriptor+UserRecordFields.swift"; sourceTree = ""; }; 848362FC2262A30800DA1D35 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = ""; }; 848362FE2262A30E00DA1D35 /* template.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = template.html; sourceTree = ""; }; @@ -1538,8 +1528,6 @@ 84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = establishMainWindowStartingState.applescript; sourceTree = ""; }; 84F9EAE4213660A100CF2DE4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconURLFinder.swift; sourceTree = ""; }; - 9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsFeedlyWebWindowController.swift; sourceTree = ""; }; - 9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsFeedlyWeb.xib; sourceTree = ""; }; B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = ""; }; B24EFD5923310109006C6242 /* WKPreferencesPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKPreferencesPrivate.h; sourceTree = ""; }; B528F81D23333C7E00E735DD /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = ""; }; @@ -1807,7 +1795,6 @@ 51A16990235E10D600EB091F /* Settings.storyboard */, 51A16995235E10D600EB091F /* AboutViewController.swift */, 51A16992235E10D600EB091F /* AddAccountViewController.swift */, - 51A1698D235E10D600EB091F /* RefreshIntervalViewController.swift */, 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */, 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */, 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */, @@ -1838,15 +1825,6 @@ path = Activity; sourceTree = ""; }; - 519D740423243C68008BB345 /* Model Extensions */ = { - isa = PBXGroup; - children = ( - 519D740523243CC0008BB345 /* RefreshInterval-Extensions.swift */, - 51AF460D232488C6001742EF /* Account-Extensions.swift */, - ); - path = "Model Extensions"; - sourceTree = ""; - }; 51C45245226506C800C03939 /* UIKit Extensions */ = { isa = PBXGroup; children = ( @@ -1930,7 +1908,6 @@ children = ( 51C4527E2265092C00C03939 /* ArticleViewController.swift */, 517630222336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift */, - 5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */, 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */, 5142192923522B5500E07E2C /* ImageViewController.swift */, 514219362352510100E07E2C /* ImageScrollView.swift */, @@ -1954,10 +1931,11 @@ 51C452A822650DA100C03939 /* Article Rendering */ = { isa = PBXGroup; children = ( - 49F40DEF2335B71000552BF4 /* newsfoot.js */, + 5141E7552374A2890013FF27 /* ArticleIconSchemeHandler.swift */, 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */, - 848362FE2262A30E00DA1D35 /* template.html */, 517630032336215100E15FFF /* main.js */, + 49F40DEF2335B71000552BF4 /* newsfoot.js */, + 848362FE2262A30E00DA1D35 /* template.html */, ); path = "Article Rendering"; sourceTree = ""; @@ -2088,6 +2066,7 @@ 519B8D322143397200FA689C /* SharingServiceDelegate.swift */, 849EE72020391F560082A1EA /* SharingServicePickerDelegate.swift */, 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */, + 847CD6C9232F4CBF00FAC46D /* IconView.swift */, 844B5B6B1FEA224B00C7C76A /* Keyboard */, 849A975F1ED9EB95007D329B /* Sidebar */, 849A97681ED9EBC8007D329B /* Timeline */, @@ -2265,7 +2244,6 @@ 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */, 849A97711ED9EC04007D329B /* TimelineCellData.swift */, 849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */, - 847CD6C9232F4CBF00FAC46D /* TimelineIconView.swift */, ); path = Cell; sourceTree = ""; @@ -2510,8 +2488,6 @@ 5144EA2E2279FAB600D19003 /* AccountsDetailViewController.swift */, 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */, 5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */, - 9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */, - 9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */, 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */, 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */, 5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */, @@ -2566,7 +2542,6 @@ 5123DB95233EC69300282CC9 /* Inspector */, 513145F9235A55A700387FDC /* Intents */, 5183CCEB227117C70010922C /* Settings */, - 519D740423243C68008BB345 /* Model Extensions */, 51C45245226506C800C03939 /* UIKit Extensions */, 513C5CE7232571C2003D4054 /* ShareExtension */, 51314643235A7C2300387FDC /* IntentsExtension */, @@ -3359,7 +3334,6 @@ 65ED4050235DEF6C0081F399 /* DetailKeyboardShortcuts.plist in Resources */, 65ED4051235DEF6C0081F399 /* TimelineKeyboardShortcuts.plist in Resources */, 65ED4052235DEF6C0081F399 /* template.html in Resources */, - 65ED4053235DEF6C0081F399 /* AccountsFeedlyWeb.xib in Resources */, 65ED4054235DEF6C0081F399 /* Main.storyboard in Resources */, 65ED4055235DEF6C0081F399 /* AccountsAdd.xib in Resources */, 65ED4056235DEF6C0081F399 /* NetNewsWire.sdef in Resources */, @@ -3445,7 +3419,6 @@ 5127B23A222B4849006D641D /* DetailKeyboardShortcuts.plist in Resources */, 845479881FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist in Resources */, 848362FF2262A30E00DA1D35 /* template.html in Resources */, - 9EA33BBA2318F8C10097B644 /* AccountsFeedlyWeb.xib in Resources */, 848363082262A3DD00DA1D35 /* Main.storyboard in Resources */, 51EF0F8E2279C9260050506E /* AccountsAdd.xib in Resources */, 84C9FC8F22629E8F00D921D6 /* NetNewsWire.sdef in Resources */, @@ -3743,7 +3716,7 @@ files = ( 65ED3FB7235DEF6C0081F399 /* ArticleArray.swift in Sources */, 65ED3FB8235DEF6C0081F399 /* CrashReporter.swift in Sources */, - 65ED3FB9235DEF6C0081F399 /* TimelineIconView.swift in Sources */, + 65ED3FB9235DEF6C0081F399 /* IconView.swift in Sources */, 65ED3FBA235DEF6C0081F399 /* ArticleExtractorConfig.swift in Sources */, 65ED3FBB235DEF6C0081F399 /* InspectorWindowController.swift in Sources */, 65ED3FBC235DEF6C0081F399 /* ColorHash.swift in Sources */, @@ -3762,7 +3735,6 @@ 65ED3FC9235DEF6C0081F399 /* SmartFeedPasteboardWriter.swift in Sources */, 65ED3FCA235DEF6C0081F399 /* SmartFeedsController.swift in Sources */, 65ED3FCB235DEF6C0081F399 /* SidebarViewController.swift in Sources */, - 65ED3FCC235DEF6C0081F399 /* AccountsFeedlyWebWindowController.swift in Sources */, 65ED3FCD235DEF6C0081F399 /* SidebarOutlineView.swift in Sources */, 65ED3FCE235DEF6C0081F399 /* DetailKeyboardDelegate.swift in Sources */, 65ED3FCF235DEF6C0081F399 /* TimelineContainerView.swift in Sources */, @@ -3793,8 +3765,8 @@ 65ED3FE8235DEF6C0081F399 /* ArticleStyle.swift in Sources */, 65ED3FE9235DEF6C0081F399 /* FaviconURLFinder.swift in Sources */, 65ED3FEA235DEF6C0081F399 /* SidebarViewController+ContextualMenus.swift in Sources */, - 65ED3FEB235DEF6C0081F399 /* (null) in Sources */, 65ED3FEC235DEF6C0081F399 /* RSHTMLMetadata+Extension.swift in Sources */, + 518C3194237B00DA004D740F /* ArticleIconSchemeHandler.swift in Sources */, 65ED3FED235DEF6C0081F399 /* SendToMarsEditCommand.swift in Sources */, 65ED3FEE235DEF6C0081F399 /* UserNotificationManager.swift in Sources */, 65ED3FEF235DEF6C0081F399 /* ScriptingObjectContainer.swift in Sources */, @@ -3913,7 +3885,6 @@ 51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */, 517630232336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift in Sources */, 51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */, - 51AF460E232488C6001742EF /* Account-Extensions.swift in Sources */, 51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */, 513146B2235A81A400387FDC /* AddFeedIntentHandler.swift in Sources */, 51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */, @@ -3988,20 +3959,17 @@ 51A1699B235E10D700EB091F /* AccountInspectorViewController.swift in Sources */, 51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */, 5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */, - 5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */, 51C452882265093600C03939 /* AddFeedViewController.swift in Sources */, 51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */, 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */, 5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */, 518651DA235621840078E021 /* ImageTransition.swift in Sources */, 514219372352510100E07E2C /* ImageScrollView.swift in Sources */, - 51A16997235E10D700EB091F /* RefreshIntervalViewController.swift in Sources */, 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, 84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */, 512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */, 516AE9E02372269A007DEEAA /* IconImage.swift in Sources */, - 519D740623243CC0008BB345 /* RefreshInterval-Extensions.swift in Sources */, 51C45268226508F600C03939 /* MasterFeedUnreadCountView.swift in Sources */, 5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */, 51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */, @@ -4032,7 +4000,7 @@ files = ( 84F204E01FAACBB30076E152 /* ArticleArray.swift in Sources */, 848B937221C8C5540038DC0D /* CrashReporter.swift in Sources */, - 847CD6CA232F4CBF00FAC46D /* TimelineIconView.swift in Sources */, + 847CD6CA232F4CBF00FAC46D /* IconView.swift in Sources */, 51FA73AA2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */, 84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */, 51EF0F7A22771B890050506E /* ColorHash.swift in Sources */, @@ -4053,7 +4021,6 @@ 849C78922362AB04009A71E4 /* ExportOPMLWindowController.swift in Sources */, 84CC88181FE59CBF00644329 /* SmartFeedsController.swift in Sources */, 849A97661ED9EB96007D329B /* SidebarViewController.swift in Sources */, - 9EA33BB92318F8C10097B644 /* AccountsFeedlyWebWindowController.swift in Sources */, 849A97641ED9EB96007D329B /* SidebarOutlineView.swift in Sources */, 5127B238222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */, 8405DD9922153B6B008CE1BF /* TimelineContainerView.swift in Sources */, @@ -4128,6 +4095,7 @@ 848D578E21543519005FFAD5 /* PasteboardFeed.swift in Sources */, 5144EA2F2279FAB600D19003 /* AccountsDetailViewController.swift in Sources */, 849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */, + 518C3193237B00D9004D740F /* ArticleIconSchemeHandler.swift in Sources */, 84C9FC6722629B9000D921D6 /* AppDelegate.swift in Sources */, 84C9FC7A22629E1200D921D6 /* AccountsTableViewBackgroundView.swift in Sources */, 84CAFCAF22BC8C35007694F0 /* FetchRequestOperation.swift in Sources */, diff --git a/iOS/Article/ArticleIconSchemeHandler.swift b/Shared/Article Rendering/ArticleIconSchemeHandler.swift similarity index 100% rename from iOS/Article/ArticleIconSchemeHandler.swift rename to Shared/Article Rendering/ArticleIconSchemeHandler.swift diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift index 93d1676cf..ae74fd84c 100644 --- a/Shared/Article Rendering/ArticleRenderer.swift +++ b/Shared/Article Rendering/ArticleRenderer.swift @@ -31,9 +31,8 @@ struct ArticleRenderer { private let title: String private let body: String private let baseURL: String? - private let useImageIcon: Bool - private init(article: Article?, extractedArticle: ExtractedArticle?, style: ArticleStyle, useImageIcon: Bool = false) { + private init(article: Article?, extractedArticle: ExtractedArticle?, style: ArticleStyle) { self.article = article self.extractedArticle = extractedArticle self.articleStyle = style @@ -45,13 +44,12 @@ struct ArticleRenderer { self.body = article?.body ?? "" self.baseURL = article?.baseURL?.absoluteString } - self.useImageIcon = useImageIcon } // MARK: - API static func articleHTML(article: Article, extractedArticle: ExtractedArticle? = nil, style: ArticleStyle, useImageIcon: Bool = false) -> Rendering { - let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, style: style, useImageIcon: useImageIcon) + let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, style: style) return (renderer.styleString(), renderer.articleHTML) } @@ -104,9 +102,6 @@ private extension ArticleRenderer { return renderHTML(withBody: "") } - static var faviconImgTagCache = [Feed: String]() - static var feedIconImgTagCache = [Feed: String]() - static var defaultStyleSheet: String = { let path = Bundle.main.path(forResource: "styleSheet", ofType: "css")! let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) @@ -146,13 +141,7 @@ private extension ArticleRenderer { d["title"] = title d["body"] = body - - d["avatars"] = "" - var didAddAvatar = false - if let avatarHTML = avatarImgTag() { - d["avatars"] = "\(avatarHTML)"; - didAddAvatar = true - } + d["avatars"] = ""; var feedLink = "" if let feedTitle = article.feed?.nameForDisplay { @@ -163,12 +152,6 @@ private extension ArticleRenderer { } d["feedlink"] = feedLink - if !didAddAvatar, let feed = article.feed { - if let favicon = faviconImgTag(forFeed: feed) { - d["avatars"] = "\(favicon)"; - } - } - let datePublished = article.logicalDatePublished let longDate = dateString(datePublished, .long, .medium) let mediumDate = dateString(datePublished, .medium, .short) @@ -200,111 +183,6 @@ private extension ArticleRenderer { return permalink != preferredLink // Make date a link if it’s a different link from the title’s link } - func faviconImgTag(forFeed feed: Feed) -> String? { - - if let cachedImgTag = ArticleRenderer.faviconImgTagCache[feed] { - return cachedImgTag - } - - if let iconImage = appDelegate.faviconDownloader.faviconAsIcon(for: feed) { - if let s = base64String(forImage: iconImage.image) { - var dimension = min(iconImage.image.size.height, CGFloat(ArticleRenderer.avatarDimension)) // Assuming square images. - dimension = max(dimension, 16) // Some favicons say they’re < 16. Force them larger. - if dimension >= CGFloat(ArticleRenderer.avatarDimension) * 0.8 { //Close enough to scale up. - dimension = CGFloat(ArticleRenderer.avatarDimension) - } - - let imgTag: String - if dimension >= CGFloat(ArticleRenderer.avatarDimension) { - // Use rounded corners. - imgTag = "" - } - else { - imgTag = "" - } - ArticleRenderer.faviconImgTagCache[feed] = imgTag - return imgTag - } - } - - return nil - } - - func feedIconImgTag(forFeed feed: Feed) -> String? { - if let cachedImgTag = ArticleRenderer.feedIconImgTagCache[feed] { - return cachedImgTag - } - - if useImageIcon { - return "" - } - - if let iconImage = appDelegate.feedIconDownloader.icon(for: feed) { - if let s = base64String(forImage: iconImage.image) { - #if os(macOS) - let imgTag = "" - #else - let imgTag = "" - #endif - ArticleRenderer.feedIconImgTagCache[feed] = imgTag - return imgTag - } - } - - return nil - } - - func base64String(forImage image: RSImage) -> String? { - return image.dataRepresentation()?.base64EncodedString() - } - - func singleArticleSpecifiedAuthor() -> Author? { - // The author of this article, if just one. - if let authors = article?.authors, authors.count == 1 { - return authors.first! - } - return nil - } - - func singleFeedSpecifiedAuthor() -> Author? { - if let authors = article?.feed?.authors, authors.count == 1 { - return authors.first! - } - return nil - } - - static let avatarDimension = 48 - - struct Avatar { - let imageURL: String - let url: String? - - func html(dimension: Int) -> String { - let imageTag = "" - if let url = url { - return imageTag.htmlByAddingLink(url) - } - return imageTag - } - } - - func avatarImgTag() -> String? { - if let author = singleArticleSpecifiedAuthor(), let authorImageURL = author.avatarURL { - let imageURL = useImageIcon ? ArticleRenderer.imageIconScheme : authorImageURL - return Avatar(imageURL: imageURL, url: author.url).html(dimension: ArticleRenderer.avatarDimension) - } - if let feed = article?.feed, let imgTag = feedIconImgTag(forFeed: feed) { - return imgTag - } - if let feedIconURL = article?.feed?.iconURL { - return Avatar(imageURL: feedIconURL, url: article?.feed?.homePageURL ?? article?.feed?.url).html(dimension: ArticleRenderer.avatarDimension) - } - if let author = singleFeedSpecifiedAuthor(), let imageURL = author.avatarURL { - return Avatar(imageURL: imageURL, url: author.url).html(dimension: ArticleRenderer.avatarDimension) - } - return nil - } - func byline() -> String { guard let authors = article?.authors ?? article?.feed?.authors, !authors.isEmpty else { return "" diff --git a/Shared/Data/ArticleUtilities.swift b/Shared/Data/ArticleUtilities.swift index e954f215b..51c598115 100644 --- a/Shared/Data/ArticleUtilities.swift +++ b/Shared/Data/ArticleUtilities.swift @@ -94,7 +94,7 @@ extension Article { } } -// MARK: PathIDUserInfoProvider +// MARK: DeepLinkProvider extension Article: DeepLinkProvider { diff --git a/Shared/Data/SmallIconProvider.swift b/Shared/Data/SmallIconProvider.swift index 968a9767c..a070dec30 100644 --- a/Shared/Data/SmallIconProvider.swift +++ b/Shared/Data/SmallIconProvider.swift @@ -22,11 +22,7 @@ extension Feed: SmallIconProvider { if let iconImage = appDelegate.faviconDownloader.favicon(for: self) { return iconImage } - #if os(macOS) - return AppAssets.genericFeedImage - #else return FaviconGenerator.favicon(self) - #endif } } diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 36e6c057a..20993af6f 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -23,6 +23,10 @@ struct AppAssets { return UIImage(named: "accountFeedbin")! }() + static var accountFeedlyImage: UIImage = { + return UIImage(named: "accountFeedly")! + }() + static var accountFreshRSSImage: UIImage = { return UIImage(named: "accountFreshRSS")! }() @@ -200,6 +204,8 @@ struct AppAssets { } case .feedbin: return AppAssets.accountFeedbinImage + case .feedly: + return AppAssets.accountFeedlyImage case .freshRSS: return AppAssets.accountFreshRSSImage default: diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index d68d1bd48..e2e576835 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -24,7 +24,6 @@ struct AppDefaults { static let timelineIconSize = "timelineIconSize" static let timelineSortDirection = "timelineSortDirection" static let displayUndoAvailableTip = "displayUndoAvailableTip" - static let refreshInterval = "refreshInterval" static let lastRefresh = "lastRefresh" } @@ -45,16 +44,6 @@ struct AppDefaults { } } - static var refreshInterval: RefreshInterval { - get { - let rawValue = AppDefaults.shared.integer(forKey: Key.refreshInterval) - return RefreshInterval(rawValue: rawValue) ?? RefreshInterval.everyHour - } - set { - AppDefaults.shared.set(newValue.rawValue, forKey: Key.refreshInterval) - } - } - static var timelineGroupByFeed: Bool { get { return bool(for: Key.timelineGroupByFeed) @@ -112,7 +101,6 @@ struct AppDefaults { static func registerDefaults() { let defaults: [String : Any] = [Key.lastImageCacheFlushDate: Date(), - Key.refreshInterval: RefreshInterval.everyHour.rawValue, Key.timelineGroupByFeed: false, Key.timelineNumberOfLines: 2, Key.timelineIconSize: MasterTimelineIconSize.medium.rawValue, diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 58c741ea4..8fc666e1b 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -290,9 +290,9 @@ private extension AppDelegate { /// Schedules a background app refresh based on `AppDefaults.refreshInterval`. func scheduleBackgroundFeedRefresh() { let request = BGAppRefreshTaskRequest(identifier: "com.ranchero.NetNewsWire.FeedRefresh") - request.earliestBeginDate = Date(timeIntervalSinceNow: AppDefaults.refreshInterval.inSeconds()) + request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) - // We send this to a dedicated seria queue because as of 11/05/19 on iOS 13.2 the call to the + // We send this to a dedicated serial queue because as of 11/05/19 on iOS 13.2 the call to the // task scheduler can hang indefinitely. bgTaskDispatchQueue.async { do { diff --git a/iOS/Inspector/FeedInspectorViewController.swift b/iOS/Inspector/FeedInspectorViewController.swift index 2aa9dab67..d52a8275f 100644 --- a/iOS/Inspector/FeedInspectorViewController.swift +++ b/iOS/Inspector/FeedInspectorViewController.swift @@ -11,7 +11,7 @@ import Account class FeedInspectorViewController: UITableViewController { - static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0) + static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0) var feed: Feed! @IBOutlet weak var nameTextField: UITextField! diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index f97341272..334ebbeff 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -58,17 +58,17 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { NotificationCenter.default.addObserver(self, selector: #selector(feedMetadataDidChange(_:)), name: .FeedMetadataDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged) configureToolbar() becomeFirstResponder() + } override func viewWillAppear(_ animated: Bool) { - navigationController?.title = NSLocalizedString("Feeds", comment: "Feeds") - applyChanges(animate: false) updateUI() super.viewWillAppear(animated) } @@ -99,8 +99,17 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject) } - if let node = node, dataSource.indexPath(for: node) != nil { - reloadNode(node) + // Only do the reload of the node when absolutely necessary. It can stop programatic scrolling from + // completing if called to soon after a selectRow where scrolling is necessary. See discloseFeed. + if let node = node, + let indexPath = dataSource.indexPath(for: node), + let cell = tableView.cellForRow(at: indexPath) as? MasterFeedTableViewCell, + let unreadCountProvider = node.representedObject as? UnreadCountProvider { + + if cell.unreadCount != unreadCountProvider.unreadCount { + self.reloadNode(node) + } + } } @@ -140,6 +149,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { applyChanges(animate: false) } + @objc func willEnterForeground(_ note: Notification) { + updateUI() + } + // MARK: Table View override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { @@ -457,14 +470,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } } - func updateFeedSelection() { + func updateFeedSelection(animated: Bool) { if dataSource.snapshot().numberOfItems > 0 { if let indexPath = coordinator.currentFeedIndexPath { if tableView.indexPathForSelectedRow != indexPath { - tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true) + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: animated) } } else { - tableView.selectRow(at: nil, animated: true, scrollPosition: .none) + tableView.selectRow(at: nil, animated: animated, scrollPosition: .none) } } } @@ -1017,7 +1030,7 @@ private extension MasterFeedViewController { deleteCommand.perform() if indexPath == coordinator.currentFeedIndexPath { - coordinator.selectFeed(nil) + coordinator.selectFeed(nil, animated: false) } } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index bf23f7dd1..c1613e66d 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -25,6 +25,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner weak var coordinator: SceneCoordinator! var undoableCommands = [UndoableCommand]() + let scrollPositionQueue = CoalescingQueue(name: "Scroll Position", interval: 0.3, maxInterval: 1.0) private let keyboardManager = KeyboardManager(type: .timeline) override var keyCommands: [UIKeyCommand]? { @@ -68,14 +69,14 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner resetEstimatedRowHeight() resetUI() - - } - - override func viewWillAppear(_ animated: Bool) { + applyChanges(animate: false) - if dataSource.snapshot().numberOfItems < 1 { - navigationItem.searchController?.isActive = false + + // Restore the scroll position if we have one stored + if let restoreIndexPath = coordinator.timelineMiddleIndexPath { + tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false) } + } // MARK: Actions @@ -288,6 +289,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner coordinator.selectArticle(article, animated: true) } + override func scrollViewDidScroll(_ scrollView: UIScrollView) { + scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) + } + // MARK: Notifications @objc dynamic func unreadCountDidChange(_ notification: Notification) { @@ -366,6 +371,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner titleView?.label.text = coordinator.timelineName } + @objc func scrollPositionDidChange() { + coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow() + } + // MARK: Reloading func queueReloadAvailableCells() { @@ -583,9 +592,8 @@ private extension MasterTimelineViewController { func discloseFeedAction(_ article: Article) -> UIAction? { guard let feed = article.feed else { return nil } - let title = NSLocalizedString("Select Feed", comment: "Select Feed") + let title = NSLocalizedString("Go to Feed", comment: "Go to Feed") let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in - self?.coordinator.selectFeed(nil, animated: true) self?.coordinator.discloseFeed(feed, animated: true) } return action @@ -594,9 +602,8 @@ private extension MasterTimelineViewController { func discloseFeedAlertAction(_ article: Article, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? { guard let feed = article.feed else { return nil } - let title = NSLocalizedString("Select Feed", comment: "Select Feed") + let title = NSLocalizedString("Go to Feed", comment: "Go to Feed") let action = UIAlertAction(title: title, style: .default) { [weak self] action in - self?.coordinator.selectFeed(nil, animated: true) self?.coordinator.discloseFeed(feed, animated: true) completionHandler(true) } diff --git a/iOS/Model Extensions/Account-Extensions.swift b/iOS/Model Extensions/Account-Extensions.swift deleted file mode 100644 index bdad16270..000000000 --- a/iOS/Model Extensions/Account-Extensions.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Account-Extensions.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 9/7/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import Foundation -import Account - -extension AccountType: Identifiable { - public var id: Int { - return rawValue - } -} - -extension Account: Identifiable { - public var id: String { - return accountID - } -} diff --git a/iOS/Model Extensions/RefreshInterval-Extensions.swift b/iOS/Model Extensions/RefreshInterval-Extensions.swift deleted file mode 100644 index 5906d10a2..000000000 --- a/iOS/Model Extensions/RefreshInterval-Extensions.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// RefreshInterval-Extensions.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 9/7/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import Foundation - -extension RefreshInterval: Identifiable { - var id: Int { - return rawValue - } -} diff --git a/iOS/Resources/Assets.xcassets/accountFeedly.imageset/Contents.json b/iOS/Resources/Assets.xcassets/accountFeedly.imageset/Contents.json new file mode 100644 index 000000000..754629a4a --- /dev/null +++ b/iOS/Resources/Assets.xcassets/accountFeedly.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "accountFeedly.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/iOS/Resources/Assets.xcassets/accountFeedly.imageset/accountFeedly.pdf b/iOS/Resources/Assets.xcassets/accountFeedly.imageset/accountFeedly.pdf new file mode 100644 index 000000000..907e486bc Binary files /dev/null and b/iOS/Resources/Assets.xcassets/accountFeedly.imageset/accountFeedly.pdf differ diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index b57d60ed3..d84c0636a 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -89,9 +89,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } private let treeControllerDelegate = FeedTreeControllerDelegate() - private lazy var treeController: TreeController = { - return TreeController(delegate: treeControllerDelegate) - }() + private let treeController: TreeController var stateRestorationActivity: NSUserActivity? { return activityManager.stateRestorationActivity @@ -135,6 +133,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { var timelineFetcher: ArticleFetcher? { didSet { + timelineMiddleIndexPath = nil + if timelineFetcher is Feed { showFeedNames = false } else { @@ -153,6 +153,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } } + var timelineMiddleIndexPath: IndexPath? + private(set) var showFeedNames = false private(set) var showIcons = false @@ -276,6 +278,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } override init() { + treeController = TreeController(delegate: treeControllerDelegate) + super.init() for section in treeController.rootNode.childNodes { @@ -311,10 +315,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { masterFeedViewController = UIStoryboard.main.instantiateController(ofType: MasterFeedViewController.self) masterFeedViewController.coordinator = self masterNavigationController.pushViewController(masterFeedViewController, animated: false) + masterFeedViewController.reloadFeeds() let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) articleViewController.coordinator = self - let detailNavigationController = addNavControllerIfNecessary(articleViewController, showButton: false) + let detailNavigationController = addNavControllerIfNecessary(articleViewController, showButton: true) rootSplitViewController.showDetailViewController(detailNavigationController, sender: self) configureThreePanelMode(for: size) @@ -323,7 +328,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func handle(_ activity: NSUserActivity) { - selectFeed(nil) + selectFeed(nil, animated: false) guard let activityType = ActivityType(rawValue: activity.activityType) else { return } switch activityType { @@ -367,12 +372,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func selectFirstUnreadInAllUnread() { - selectFeed(IndexPath(row: 1, section: 0)) + selectFeed(IndexPath(row: 1, section: 0), animated: false) selectFirstUnreadArticleInTimeline() } func showSearch() { - selectFeed(nil) + selectFeed(nil, animated: false) installTimelineControllerIfNecessary(animated: false) DispatchQueue.main.asyncAfter(deadline: .now()) { self.masterTimelineViewController!.showSearchAll() @@ -528,13 +533,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return indexPathFor(node) } - func selectFeed(_ indexPath: IndexPath?, animated: Bool = false) { - guard indexPath != currentFeedIndexPath else { return } + func selectFeed(_ indexPath: IndexPath?, animated: Bool) { + guard indexPath != currentFeedIndexPath else { return } selectArticle(nil) currentFeedIndexPath = indexPath - masterFeedViewController.updateFeedSelection() + masterFeedViewController.updateFeedSelection(animated: animated) if let ip = indexPath, let node = nodeFor(ip), let fetcher = node.representedObject as? ArticleFetcher { timelineFetcher = fetcher @@ -552,31 +557,31 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func selectPrevFeed() { if let indexPath = prevFeedIndexPath { - selectFeed(indexPath) + selectFeed(indexPath, animated: true) } } func selectNextFeed() { if let indexPath = nextFeedIndexPath { - selectFeed(indexPath) + selectFeed(indexPath, animated: true) } } func selectTodayFeed() { masterFeedViewController?.ensureSectionIsExpanded(0) { - self.selectFeed(IndexPath(row: 0, section: 0)) + self.selectFeed(IndexPath(row: 0, section: 0), animated: true) } } func selectAllUnreadFeed() { masterFeedViewController?.ensureSectionIsExpanded(0) { - self.selectFeed(IndexPath(row: 1, section: 0)) + self.selectFeed(IndexPath(row: 1, section: 0), animated: true) } } func selectStarredFeed() { masterFeedViewController?.ensureSectionIsExpanded(0) { - self.selectFeed(IndexPath(row: 2, section: 0)) + self.selectFeed(IndexPath(row: 2, section: 0), animated: true) } } @@ -819,14 +824,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { let feedInspectorNavController = UIStoryboard.inspector.instantiateViewController(identifier: "FeedInspectorNavigationViewController") as! UINavigationController let feedInspectorController = feedInspectorNavController.topViewController as! FeedInspectorViewController - feedInspectorController.modalPresentationStyle = .formSheet - feedInspectorController.preferredContentSize = FeedInspectorViewController.preferredContentSizeForFormSheetDisplay + feedInspectorNavController.modalPresentationStyle = .formSheet + feedInspectorNavController.preferredContentSize = FeedInspectorViewController.preferredContentSizeForFormSheetDisplay feedInspectorController.feed = feed rootSplitViewController.present(feedInspectorNavController, animated: true) } func showAdd(_ type: AddControllerType, initialFeed: String? = nil, initialFeedName: String? = nil) { - selectFeed(nil) + selectFeed(nil, animated: false) let addViewController = UIStoryboard.add.instantiateInitialViewController() as! UINavigationController @@ -965,7 +970,7 @@ extension SceneCoordinator: UINavigationControllerDelegate { // If we are showing the Feeds and only the feeds start clearing stuff if viewController === masterFeedViewController && !isThreePanelMode && !isTimelineViewControllerPending { activityManager.invalidateCurrentActivities() - selectFeed(nil) + selectFeed(nil, animated: true) return } @@ -1200,7 +1205,7 @@ private extension SceneCoordinator { } if unreadCountProvider.unreadCount > 0 { - selectFeed(prevIndexPath) + selectFeed(prevIndexPath, animated: true) return true } @@ -1306,7 +1311,7 @@ private extension SceneCoordinator { } if unreadCountProvider.unreadCount > 0 { - selectFeed(nextIndexPath) + selectFeed(nextIndexPath, animated: true) return true } @@ -1488,13 +1493,15 @@ private extension SceneCoordinator { // MARK: Double Split func installTimelineControllerIfNecessary(animated: Bool) { - - isTimelineViewControllerPending = true - if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 { + + isTimelineViewControllerPending = true + masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) masterTimelineViewController!.coordinator = self navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: animated) + + masterTimelineViewController?.reloadArticles(animate: false) } } @@ -1589,7 +1596,7 @@ private extension SceneCoordinator { subSplitViewController!.showDetailViewController(navController, sender: self) masterFeedViewController.restoreSelectionIfNecessary(adjustScroll: true) - masterTimelineViewController!.restoreSelectionIfNecessary(adjustScroll: true) + masterTimelineViewController!.restoreSelectionIfNecessary(adjustScroll: false) // We made sure this was there above when we called configureDoubleSplit return subSplitViewController! @@ -1642,19 +1649,19 @@ private extension SceneCoordinator { func handleSelectToday() { if let indexPath = indexPathFor(SmartFeedsController.shared.todayFeed) { - selectFeed(indexPath) + selectFeed(indexPath, animated: false) } } func handleSelectAllUnread() { if let indexPath = indexPathFor(SmartFeedsController.shared.unreadFeed) { - selectFeed(indexPath) + selectFeed(indexPath, animated: false) } } func handleSelectStarred() { if let indexPath = indexPathFor(SmartFeedsController.shared.starredFeed) { - selectFeed(indexPath) + selectFeed(indexPath, animated: false) } } @@ -1663,7 +1670,7 @@ private extension SceneCoordinator { return } if let indexPath = indexPathFor(folderNode) { - selectFeed(indexPath) + selectFeed(indexPath, animated: false) } } diff --git a/iOS/Settings/AddAccountViewController.swift b/iOS/Settings/AddAccountViewController.swift index f53d46a55..0f803b8a6 100644 --- a/iOS/Settings/AddAccountViewController.swift +++ b/iOS/Settings/AddAccountViewController.swift @@ -38,6 +38,11 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate let addViewController = navController.topViewController as! FeedbinAccountViewController addViewController.delegate = self present(navController, animated: true) + case 2: + let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly) + addAccount.delegate = self + addAccount.presentationAnchor = self.view.window! + OperationQueue.main.addOperation(addAccount) default: break } @@ -48,3 +53,28 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate } } + +extension AddAccountViewController: OAuthAccountAuthorizationOperationDelegate { + + func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) { + let rootViewController = view.window?.rootViewController + + account.refreshAll { result in + switch result { + case .success: + break + case .failure(let error): + guard let viewController = rootViewController else { + return + } + viewController.presentError(error) + } + } + + dismiss() + } + + func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) { + presentError(error) + } +} diff --git a/iOS/Settings/RefreshIntervalViewController.swift b/iOS/Settings/RefreshIntervalViewController.swift deleted file mode 100644 index 8f3d89ba9..000000000 --- a/iOS/Settings/RefreshIntervalViewController.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// RefreshIntervalViewController.swift -// NetNewsWire -// -// Created by Maurice Parker on 4/25/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import UIKit - -class RefreshIntervalViewController: UITableViewController { - - override func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 7 - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - - let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) - - cell.textLabel?.adjustsFontForContentSizeCategory = true - - let userRefreshInterval = AppDefaults.refreshInterval - - switch indexPath.row { - case 0: - cell.textLabel?.text = RefreshInterval.manually.description() - if userRefreshInterval == RefreshInterval.manually { - cell.accessoryType = .checkmark - } else { - cell.accessoryType = .none - } - case 1: - cell.textLabel?.text = RefreshInterval.every10Minutes.description() - if userRefreshInterval == RefreshInterval.every10Minutes { - cell.accessoryType = .checkmark - } else { - cell.accessoryType = .none - } - case 2: - cell.textLabel?.text = RefreshInterval.every30Minutes.description() - if userRefreshInterval == RefreshInterval.every30Minutes { - cell.accessoryType = .checkmark - } else { - cell.accessoryType = .none - } - case 3: - cell.textLabel?.text = RefreshInterval.everyHour.description() - if userRefreshInterval == RefreshInterval.everyHour { - cell.accessoryType = .checkmark - } else { - cell.accessoryType = .none - } - case 4: - cell.textLabel?.text = RefreshInterval.every2Hours.description() - if userRefreshInterval == RefreshInterval.every2Hours { - cell.accessoryType = .checkmark - } else { - cell.accessoryType = .none - } - case 5: - cell.textLabel?.text = RefreshInterval.every4Hours.description() - if userRefreshInterval == RefreshInterval.every4Hours { - cell.accessoryType = .checkmark - } else { - cell.accessoryType = .none - } - default: - cell.textLabel?.text = RefreshInterval.every8Hours.description() - if userRefreshInterval == RefreshInterval.every8Hours { - cell.accessoryType = .checkmark - } else { - cell.accessoryType = .none - } - } - - return cell - - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - - let refreshInterval: RefreshInterval - - switch indexPath.row { - case 0: - refreshInterval = RefreshInterval.manually - case 1: - refreshInterval = RefreshInterval.every10Minutes - case 2: - refreshInterval = RefreshInterval.every30Minutes - case 3: - refreshInterval = RefreshInterval.everyHour - case 4: - refreshInterval = RefreshInterval.every2Hours - case 5: - refreshInterval = RefreshInterval.every4Hours - default: - refreshInterval = RefreshInterval.every8Hours - } - - AppDefaults.refreshInterval = refreshInterval - self.navigationController?.popViewController(animated: true) - - } - -} diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index ff43a99bd..bcb9ff294 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -60,32 +60,8 @@ - - - - - - - - - - - - - + @@ -102,7 +78,7 @@ - + @@ -119,7 +95,7 @@ - + @@ -140,7 +116,7 @@ - + @@ -171,7 +147,7 @@ - + @@ -202,7 +178,7 @@ - + @@ -223,7 +199,7 @@ - + @@ -240,7 +216,7 @@ - + @@ -257,7 +233,7 @@ - + @@ -274,7 +250,7 @@ - + @@ -291,7 +267,7 @@ - + @@ -308,7 +284,7 @@ - + @@ -424,6 +400,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -441,35 +450,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -647,7 +627,7 @@ - + @@ -750,7 +730,7 @@ - + @@ -806,11 +786,12 @@ - + + diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index 2a36606c8..ec690552a 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -104,16 +104,6 @@ class SettingsViewController: UITableViewController { cell = acctCell } - case 2: - - if indexPath.row == 0 { - cell = tableView.dequeueReusableCell(withIdentifier: "SettingsTableViewCell", for: indexPath) - cell.textLabel?.text = NSLocalizedString("Refresh Interval", comment: "Refresh Interval") - cell.detailTextLabel?.text = AppDefaults.refreshInterval.description() - } else { - cell = super.tableView(tableView, cellForRowAt: indexPath) - } - default: cell = super.tableView(tableView, cellForRowAt: indexPath) @@ -141,21 +131,18 @@ class SettingsViewController: UITableViewController { case 2: switch indexPath.row { case 0: - let timeline = UIStoryboard.settings.instantiateController(ofType: RefreshIntervalViewController.self) - self.navigationController?.pushViewController(timeline, animated: true) - case 1: tableView.selectRow(at: nil, animated: true, scrollPosition: .none) if let sourceView = tableView.cellForRow(at: indexPath) { let sourceRect = tableView.rectForRow(at: indexPath) importOPML(sourceView: sourceView, sourceRect: sourceRect) } - case 2: + case 1: tableView.selectRow(at: nil, animated: true, scrollPosition: .none) if let sourceView = tableView.cellForRow(at: indexPath) { let sourceRect = tableView.rectForRow(at: indexPath) exportOPML(sourceView: sourceView, sourceRect: sourceRect) } - case 3: + case 2: addFeed() tableView.selectRow(at: nil, animated: true, scrollPosition: .none) default: diff --git a/submodules/RSCore b/submodules/RSCore index 972ff3237..ba7bbb2ce 160000 --- a/submodules/RSCore +++ b/submodules/RSCore @@ -1 +1 @@ -Subproject commit 972ff3237f819a2250e0bc1ca2814bafe328fa69 +Subproject commit ba7bbb2ce10ee04a730c0a1e425a1b2e9d338520