From 31c1dc40e36f62c6ad3040c07c2ec1d8ffa0e7a6 Mon Sep 17 00:00:00 2001 From: Phil Dokas Date: Tue, 8 Oct 2019 22:15:49 -0700 Subject: [PATCH 01/16] Fix leading for super and subscript in the article view --- Mac/MainWindow/Detail/styleSheet.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Mac/MainWindow/Detail/styleSheet.css b/Mac/MainWindow/Detail/styleSheet.css index ce5ef262e..a98d508e1 100644 --- a/Mac/MainWindow/Detail/styleSheet.css +++ b/Mac/MainWindow/Detail/styleSheet.css @@ -140,6 +140,18 @@ figcaption { line-height: 1.3em; } +sup { + vertical-align: top; + position: relative; + bottom: 0.2rem; +} + +sub { + vertical-align: bottom; + position: relative; + top: 0.2rem; +} + .iframeWrap { position: relative; display: block; From d76a12ff99be8c5ce74cbe3b63675ed166664a32 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 9 Oct 2019 01:23:05 -0500 Subject: [PATCH 02/16] Change to do debug build instead of release --- buildscripts/ci-build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildscripts/ci-build.sh b/buildscripts/ci-build.sh index 5f44bd72c..f1d5cc9ef 100755 --- a/buildscripts/ci-build.sh +++ b/buildscripts/ci-build.sh @@ -15,6 +15,6 @@ security default-keychain -s github-build.keychain rm -f ./buildscripts/certs/dev.cer rm -f ./buildscripts/certs/dev.p12 -xcodebuild -scheme 'NetNewsWire' -configuration Release -allowProvisioningUpdates -showBuildTimingSummary +xcodebuild -scheme 'NetNewsWire' -configuration Debug -allowProvisioningUpdates -showBuildTimingSummary security delete-keychain github-build.keychain \ No newline at end of file From 41ca023c311502e53653d40803163564d84aa8a5 Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Wed, 9 Oct 2019 18:38:12 +1100 Subject: [PATCH 03/16] Implements creating, updating, moving and removing feeds. --- .../Account/Account.xcodeproj/project.pbxproj | 4 + .../Account/Feedly/FeedlyAPICaller.swift | 92 +++++++++ .../Feedly/FeedlyAccountDelegate.swift | 188 +++++++++++++++++- .../Feedly/FeedlyAccountDelegateError.swift | 83 ++++++++ .../Feedly/Models/FeedlyResourceId.swift | 24 ++- 5 files changed, 376 insertions(+), 15 deletions(-) create mode 100644 Frameworks/Account/Feedly/FeedlyAccountDelegateError.swift diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 9473b2e24..e85e58d88 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ 9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */; }; 9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */; }; 9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */; }; + 9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -287,6 +288,7 @@ 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAPICaller.swift; sourceTree = ""; }; 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedlyAccountDelegate+OAuth.swift"; sourceTree = ""; }; 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAuthorizationCodeGranting.swift; sourceTree = ""; }; + 9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegateError.swift; sourceTree = ""; }; D511EEB5202422BB00712EC3 /* Account_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_debug.xcconfig; sourceTree = ""; }; D511EEB6202422BB00712EC3 /* Account_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_target.xcconfig; sourceTree = ""; }; D511EEB7202422BB00712EC3 /* Account_project_release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_release.xcconfig; sourceTree = ""; }; @@ -540,6 +542,7 @@ isa = PBXGroup; children = ( 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */, + 9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */, 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */, 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */, 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */, @@ -783,6 +786,7 @@ 8469F81C1F6DD15E0084783E /* Account.swift in Sources */, 9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */, 5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */, + 9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */, 9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */, 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */, 9E1D155923343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift in Sources */, diff --git a/Frameworks/Account/Feedly/FeedlyAPICaller.swift b/Frameworks/Account/Feedly/FeedlyAPICaller.swift index 6b7c1dd06..5ef56bb1a 100644 --- a/Frameworks/Account/Feedly/FeedlyAPICaller.swift +++ b/Frameworks/Account/Feedly/FeedlyAPICaller.swift @@ -387,6 +387,98 @@ final class FeedlyAPICaller { } } } + + func addFeed(with feedId: FeedlyFeedResourceId, title: String? = nil, toCollectionWith collectionId: String, completionHandler: @escaping (Result<[FeedlyFeed], Error>) -> ()) { + guard let accessToken = credentials?.secret else { + return DispatchQueue.main.async { + completionHandler(.failure(CredentialsError.incompleteCredentials)) + } + } + + guard let encodedId = encodeForURLPath(collectionId) else { + return DispatchQueue.main.async { + completionHandler(.failure(FeedbinAccountDelegateError.invalidParameter)) + } + } + var components = baseUrlComponents + components.percentEncodedPath = "/v3/collections/\(encodedId)/feeds" + + guard let url = components.url else { + fatalError("\(components) does not produce a valid URL.") + } + + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) + request.addValue("application/json", forHTTPHeaderField: "Accept-Type") + request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) + + do { + struct AddFeedBody: Encodable { + var id: String + var title: String? + } + let encoder = JSONEncoder() + let data = try encoder.encode(AddFeedBody(id: feedId.id, title: title)) + request.httpBody = data + } catch { + return DispatchQueue.main.async { + completionHandler(.failure(error)) + } + } + + transport.send(request: request, resultType: [FeedlyFeed].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in + switch result { + case .success(_, let collectionFeeds): + if let feeds = collectionFeeds { + completionHandler(.success(feeds)) + } else { + completionHandler(.failure(URLError(.cannotDecodeContentData))) + } + case .failure(let error): + completionHandler(.failure(error)) + } + } + } + + func removeFeed(_ feedId: String, fromCollectionWith collectionId: String, completionHandler: @escaping (Result) -> ()) { + guard let accessToken = credentials?.secret else { + return DispatchQueue.main.async { + completionHandler(.failure(CredentialsError.incompleteCredentials)) + } + } + + guard let encodedCollectionId = encodeForURLPath(collectionId), let encodedFeedId = encodeForURLPath(feedId) else { + return DispatchQueue.main.async { + completionHandler(.failure(FeedbinAccountDelegateError.invalidParameter)) + } + } + var components = baseUrlComponents + components.percentEncodedPath = "/v3/collections/\(encodedCollectionId)/feeds/\(encodedFeedId)" + + guard let url = components.url else { + fatalError("\(components) does not produce a valid URL.") + } + + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) + request.addValue("application/json", forHTTPHeaderField: "Accept-Type") + request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) + + transport.send(request: request, resultType: [FeedlyFeed].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in + switch result { + case .success(let httpResponse, _): + if httpResponse.statusCode == 200 { + completionHandler(.success(())) + } else { + completionHandler(.failure(URLError(.cannotDecodeContentData))) + } + case .failure(let error): + completionHandler(.failure(error)) + } + } + } } extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting { diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 1c6761bd1..4aef9438e 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -155,7 +155,8 @@ final class FeedlyAccountDelegate: AccountDelegate { folder.externalID = collection.id completion(.success(folder)) } else { - completion(.failure(FeedbinAccountDelegateError.invalidParameter)) + // Is the name empty? Or one of the global resource names? + completion(.failure(FeedlyAccountDelegateError.unableToAddFolder(name))) } case .failure(let error): completion(.failure(error)) @@ -165,25 +166,34 @@ final class FeedlyAccountDelegate: AccountDelegate { func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { guard let id = folder.externalID else { - completion(.failure(FeedbinAccountDelegateError.invalidParameter)) - return + return DispatchQueue.main.async { + completion(.failure(FeedlyAccountDelegateError.unableToRenameFolder(folder.nameForDisplay, name))) + } } + + let nameBefore = folder.name + caller.renameCollection(with: id, to: name) { result in switch result { case .success(let collection): folder.name = collection.label completion(.success(())) case .failure(let error): + folder.name = nameBefore completion(.failure(error)) } } + + folder.name = name } func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { guard let id = folder.externalID else { - completion(.failure(FeedbinAccountDelegateError.invalidParameter)) - return + return DispatchQueue.main.async { + completion(.failure(FeedlyAccountDelegateError.unableToRemoveFolder(folder.nameForDisplay))) + } } + caller.deleteCollection(with: id) { result in switch result { case .success: @@ -195,24 +205,182 @@ final class FeedlyAccountDelegate: AccountDelegate { } } + private func isValidContainer(for account: Account, container: Container) throws -> (Folder, String) { + guard let folder = container as? Folder else { + throw FeedlyAccountDelegateError.addFeedChooseFolder + } + + guard let collectionId = folder.externalID else { + throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder) + } + + guard let userId = credentials?.username else { + throw FeedlyAccountDelegateError.notLoggedIn + } + + let uncategorized = FeedlyCategoryResourceId.uncategorized(for: userId) + + guard collectionId != uncategorized.id else { + throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder) + } + + return (folder, collectionId) + } + func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { - fatalError() + let (folder, collectionId): (Folder, String) + do { + (folder, collectionId) = try isValidContainer(for: account, container: container) + } catch { + return DispatchQueue.main.async { + completion(.failure(error)) + } + } + + let resourceId = FeedlyFeedResourceId(url: url) + + caller.addFeed(with: resourceId, title: name, toCollectionWith: collectionId) { result in + switch result { + case .success(let feedlyFeeds): + let feedsBefore = folder.flattenedFeeds() + for feedlyFeed in feedlyFeeds where !account.hasFeed(with: feedlyFeed.feedId) { + let resourceId = FeedlyFeedResourceId(id: feedlyFeed.id) + let feed = account.createFeed(with: feedlyFeed.title, + url: resourceId.url, + feedID: feedlyFeed.id, + homePageURL: feedlyFeed.website) + folder.addFeed(feed) + } + let feedsAfter = folder.flattenedFeeds() + let added = feedsAfter.subtracting(feedsBefore) + if let feed = added.first { + completion(.success(feed)) + } else { + completion(.failure(FeedbinAccountDelegateError.invalidParameter)) + } + case .failure(let error): + completion(.failure(error)) + } + } } func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result) -> Void) { - fatalError() + let folderCollectionIds = account.folders?.filter { $0.has(feed) }.compactMap { $0.externalID } + guard let collectionIds = folderCollectionIds, let collectionId = collectionIds.first else { + completion(.failure(FeedbinAccountDelegateError.invalidParameter)) + return + } + + let feedId = FeedlyFeedResourceId(id: feed.feedID) + let editedNameBefore = feed.editedName + + caller.addFeed(with: feedId, title: name, toCollectionWith: collectionId) { result in + switch result { + case .success: + completion(.success(())) + + case .failure(let error): + feed.editedName = editedNameBefore + completion(.failure(error)) + } + } + + // optimistically set the name + feed.editedName = name } func addFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result) -> Void) { - fatalError() + let (folder, collectionId): (Folder, String) + do { + (folder, collectionId) = try isValidContainer(for: account, container: container) + } catch { + return DispatchQueue.main.async { + completion(.failure(error)) + } + } + + let feedId = FeedlyFeedResourceId(id: with.feedID) + + caller.addFeed(with: feedId, toCollectionWith: collectionId) { result in + switch result { + case .success(let feedlyFeeds): + for feedlyFeed in feedlyFeeds where !folder.hasFeed(with: feedlyFeed.feedId) { + let feed: Feed = { + if with.url == FeedlyFeedResourceId(id: feedlyFeed.id).url { + with.metadata.feedID = feedlyFeed.id + with.name = feedlyFeed.title + with.homePageURL = feedlyFeed.website + return with + } else { + let resourceId = FeedlyFeedResourceId(id: feedlyFeed.id) + return account.createFeed(with: feedlyFeed.title, + url: resourceId.url, + feedID: feedlyFeed.id, + homePageURL: feedlyFeed.website) + } + }() + folder.addFeed(feed) + } + + completion(.success(())) + + case .failure(let error): + completion(.failure(error)) + } + } } func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { - fatalError() + guard let folder = container as? Folder, let collectionId = folder.externalID else { + return DispatchQueue.main.async { + completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed))) + } + } + + caller.removeFeed(feed.feedID, fromCollectionWith: collectionId) { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + folder.addFeed(feed) + completion(.failure(error)) + } + } + + folder.removeFeed(feed) } func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result) -> Void) { - fatalError() + guard let from = from as? Folder, let to = to as? Folder else { + return DispatchQueue.main.async { + completion(.failure(FeedlyAccountDelegateError.addFeedChooseFolder)) + } + } + + addFeed(for: account, with: feed, to: to) { [weak self] addResult in + switch addResult { + // now that we have added the feed, remove it from the other collection + case .success: + self?.removeFeed(for: account, with: feed, from: from) { removeResult in + switch removeResult { + case .success: + completion(.success(())) + case .failure: + from.addFeed(feed) + completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed, from, to))) + } + } + case .failure(let error): + from.addFeed(feed) + to.removeFeed(feed) + completion(.failure(error)) + } + + } + + // optimistically move the feed, undoing as appropriate to the failure + from.removeFeed(feed) + to.addFeed(feed) } func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result) -> Void) { diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegateError.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegateError.swift new file mode 100644 index 000000000..d601f3122 --- /dev/null +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegateError.swift @@ -0,0 +1,83 @@ +// +// FeedlyAccountDelegateError.swift +// Account +// +// Created by Kiel Gillard on 9/10/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +enum FeedlyAccountDelegateError: LocalizedError { + case notLoggedIn + case unableToAddFolder(String) + case unableToRenameFolder(String, String) + case unableToRemoveFolder(String) + case unableToMoveFeedBetweenFolders(Feed, Folder, Folder) + case addFeedChooseFolder + case addFeedInvalidFolder(Folder) + case unableToRemoveFeed(Feed) + + var errorDescription: String? { + switch self { + case .notLoggedIn: + return NSLocalizedString("Please add the Feedly account again.", comment: "Feedly - Credentials not found.") + + case .unableToAddFolder(let name): + let template = NSLocalizedString("Could not create a folder named \"%@\".", comment: "Feedly - Could not create a folder/collection.") + return String(format: template, name) + + case .unableToRenameFolder(let from, let to): + let template = NSLocalizedString("Could not rename \"%@\" to \"%@\".", comment: "Feedly - Could not rename a folder/collection.") + return String(format: template, from, to) + + case .unableToRemoveFolder(let name): + let template = NSLocalizedString("Could not remove the folder named \"%@\".", comment: "Feedly - Could not remove a folder/collection.") + return String(format: template, name) + + case .unableToMoveFeedBetweenFolders(let feed, _, let to): + let template = NSLocalizedString("Could not move \"%@\" to \"%@\".", comment: "Feedly - Could not move a feed between folders/collections.") + return String(format: template, feed.nameForDisplay, to.nameForDisplay) + + case .addFeedChooseFolder: + return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly - Feed can only be added to folders.") + + case .addFeedInvalidFolder(let invalidFolder): + let template = NSLocalizedString("Feeds cannot be added to the \"%@\" folder.", comment: "Feedly - Feed can only be added to folders.") + return String(format: template, invalidFolder.nameForDisplay) + + case .unableToRemoveFeed(let feed): + let template = NSLocalizedString("Could not remove \"%@\".", comment: "Feedly - Could not remove a feed.") + return String(format: template, feed.nameForDisplay) + } + } + + var recoverySuggestion: String? { + switch self { + case .notLoggedIn: + return nil + + case .unableToAddFolder: + return nil + + case .unableToRenameFolder: + return nil + + case .unableToRemoveFolder: + return nil + + case .unableToMoveFeedBetweenFolders(let feed, let from, let to): + let template = NSLocalizedString("\"%@\" may be in both \"%@\" and \"%@\".", comment: "Feedly - Could not move a feed between folders/collections.") + return String(format: template, feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay) + + case .addFeedChooseFolder: + return nil + + case .addFeedInvalidFolder: + return NSLocalizedString("Please choose a different folder to contain the feed.", comment: "Feedly - Feed can only be added to folders recovery suggestion.") + + case .unableToRemoveFeed: + return nil + } + } +} diff --git a/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift b/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift index 3095f2709..af31c55fd 100644 --- a/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift +++ b/Frameworks/Account/Feedly/Models/FeedlyResourceId.swift @@ -13,17 +13,15 @@ protocol FeedlyResourceId { /// The resource Id from Feedly. var id: String { get } - - /// The location of the kind of resource a concrete type represents. - /// If the conrete type cannot strip the resource type from the Id, it should just return the Id - /// since the Id is a legitimate URL. - var url: String { get } } /// The Feed Resource is documented here: https://developer.feedly.com/cloud/ struct FeedlyFeedResourceId: FeedlyResourceId { var id: String + /// The location of the kind of resource a concrete type represents. + /// If the conrete type cannot strip the resource type from the Id, it should just return the Id + /// since the Id is a legitimate URL. var url: String { if let range = id.range(of: "feed/"), range.lowerBound == id.startIndex { var mutant = id @@ -35,3 +33,19 @@ struct FeedlyFeedResourceId: FeedlyResourceId { return id } } + +extension FeedlyFeedResourceId { + init(url: String) { + self.id = "feed/\(url)" + } +} + +struct FeedlyCategoryResourceId: FeedlyResourceId { + var id: String + + static func uncategorized(for userId: String) -> FeedlyCategoryResourceId { + // https://developer.feedly.com/cloud/#global-resource-ids + let id = "user/\(userId)/category/global.uncategorized" + return FeedlyCategoryResourceId(id: id) + } +} From 92fa66f3459f1035b0a7d1e4dc811a7fd1c9eac8 Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Wed, 9 Oct 2019 19:06:59 +1100 Subject: [PATCH 04/16] Refresh progress updates for changes which cannot be applied immediately and restored on failure --- .../Account/Feedly/FeedlyAccountDelegate.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 4aef9438e..b7c0dc79b 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -148,7 +148,13 @@ final class FeedlyAccountDelegate: AccountDelegate { } func addFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) { + + let progress = refreshProgress + progress.addToNumberOfTasksAndRemaining(1) + caller.createCollection(named: name) { result in + progress.completeTask() + switch result { case .success(let collection): if let folder = account.ensureFolder(with: collection.label) { @@ -194,7 +200,12 @@ final class FeedlyAccountDelegate: AccountDelegate { } } + let progress = refreshProgress + progress.addToNumberOfTasksAndRemaining(1) + caller.deleteCollection(with: id) { result in + progress.completeTask() + switch result { case .success: account.removeFolder(folder) @@ -239,7 +250,12 @@ final class FeedlyAccountDelegate: AccountDelegate { let resourceId = FeedlyFeedResourceId(url: url) + let progress = refreshProgress + progress.addToNumberOfTasksAndRemaining(1) + caller.addFeed(with: resourceId, title: name, toCollectionWith: collectionId) { result in + progress.completeTask() + switch result { case .success(let feedlyFeeds): let feedsBefore = folder.flattenedFeeds() @@ -274,6 +290,8 @@ final class FeedlyAccountDelegate: AccountDelegate { let feedId = FeedlyFeedResourceId(id: feed.feedID) let editedNameBefore = feed.editedName + // Adding an existing feed updates it. + // Updating feed name in one folder/collection updates it for all folders/collections. caller.addFeed(with: feedId, title: name, toCollectionWith: collectionId) { result in switch result { case .success: From 614628883fe46775640549f733b53bb213bc0723 Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Wed, 9 Oct 2019 19:15:48 +1100 Subject: [PATCH 05/16] Use a suitable error for unexpectedly failing to add a feed --- Frameworks/Account/Feedly/FeedlyAccountDelegate.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index b7c0dc79b..c510f7da5 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -267,13 +267,16 @@ final class FeedlyAccountDelegate: AccountDelegate { homePageURL: feedlyFeed.website) folder.addFeed(feed) } + let feedsAfter = folder.flattenedFeeds() let added = feedsAfter.subtracting(feedsBefore) + if let feed = added.first { completion(.success(feed)) } else { - completion(.failure(FeedbinAccountDelegateError.invalidParameter)) + completion(.failure(AccountError.createErrorNotFound)) } + case .failure(let error): completion(.failure(error)) } From a9656776872b88b408cfc08678f6f91b8f2d27c9 Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Wed, 9 Oct 2019 19:38:16 +1100 Subject: [PATCH 06/16] Fetch the contents of the feed when first adding it --- .../Account/Feedly/FeedlyAPICaller.swift | 13 ++++--- .../Feedly/FeedlyAccountDelegate.swift | 35 ++++++++++++++++--- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/Frameworks/Account/Feedly/FeedlyAPICaller.swift b/Frameworks/Account/Feedly/FeedlyAPICaller.swift index 5ef56bb1a..4d93167af 100644 --- a/Frameworks/Account/Feedly/FeedlyAPICaller.swift +++ b/Frameworks/Account/Feedly/FeedlyAPICaller.swift @@ -90,6 +90,11 @@ final class FeedlyAPICaller { } func getStream(for collection: FeedlyCollection, newerThan: Date? = nil, unreadOnly: Bool? = nil, completionHandler: @escaping (Result) -> ()) { + let id = FeedlyCategoryResourceId(id: collection.id) + getStream(for: id, newerThan: newerThan, unreadOnly: unreadOnly, completionHandler: completionHandler) + } + + func getStream(for resource: FeedlyResourceId, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result) -> ()) { guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completionHandler(.failure(CredentialsError.incompleteCredentials)) @@ -115,7 +120,7 @@ final class FeedlyAPICaller { queryItems.append(contentsOf: [ URLQueryItem(name: "count", value: "1000"), - URLQueryItem(name: "streamId", value: collection.id), + URLQueryItem(name: "streamId", value: resource.id), ]) components.queryItems = queryItems @@ -129,12 +134,6 @@ final class FeedlyAPICaller { request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - // URLSession.shared.dataTask(with: request) { (data, response, error) in - // let obj = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) - // let data = try! JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) - // print(String(data: data, encoding: .utf8)!) - // }.resume() - transport.send(request: request, resultType: FeedlyStream.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (_, collections)): diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index c510f7da5..32d0b459d 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -253,7 +253,7 @@ final class FeedlyAccountDelegate: AccountDelegate { let progress = refreshProgress progress.addToNumberOfTasksAndRemaining(1) - caller.addFeed(with: resourceId, title: name, toCollectionWith: collectionId) { result in + caller.addFeed(with: resourceId, title: name, toCollectionWith: collectionId) { [weak self] result in progress.completeTask() switch result { @@ -271,10 +271,35 @@ final class FeedlyAccountDelegate: AccountDelegate { let feedsAfter = folder.flattenedFeeds() let added = feedsAfter.subtracting(feedsBefore) - if let feed = added.first { - completion(.success(feed)) - } else { - completion(.failure(AccountError.createErrorNotFound)) + guard let first = added.first else { + return completion(.failure(AccountError.createErrorNotFound)) + } + + let group = DispatchGroup() + + if let self = self { + for feed in added { + group.enter() + let resourceId = FeedlyFeedResourceId(id: feed.feedID) + self.caller.getStream(for: resourceId, newerThan: nil, unreadOnly: nil) { result in + switch result { + case .success(let stream): + let items = Set(stream.items.map { FeedlyEntryParser(entry: $0).parsedItemRepresentation }) + + account.update(feed, parsedItems: items, defaultRead: false) { + group.leave() + } + + case .failure: + // Feed will remain empty until new articles appear. + group.leave() + } + } + } + } + + group.notify(queue: .main) { + completion(.success(first)) } case .failure(let error): From dedce600a42f4c2c2474713ff1d53d630d2e9e91 Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Wed, 9 Oct 2019 19:42:12 +1100 Subject: [PATCH 07/16] Implement restore feeds. Not sure how to test this. --- .../Feedly/FeedlyAccountDelegate.swift | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 32d0b459d..59e98841b 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -430,7 +430,25 @@ final class FeedlyAccountDelegate: AccountDelegate { } func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result) -> Void) { - fatalError() + if let existingFeed = account.existingFeed(withURL: feed.url) { + account.addFeed(existingFeed, to: container) { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } + } else { + createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } + } } func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> Void) { From 13197054bfa8edae406be31adab5eba6f7104b3f Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Wed, 9 Oct 2019 19:44:52 +1100 Subject: [PATCH 08/16] Implement restore folders. Not sure how to test this. --- .../Feedly/FeedlyAccountDelegate.swift | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 59e98841b..34e596440 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -452,7 +452,29 @@ final class FeedlyAccountDelegate: AccountDelegate { } func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> Void) { - fatalError() + let group = DispatchGroup() + + for feed in folder.topLevelFeeds { + + folder.topLevelFeeds.remove(feed) + + group.enter() + restoreFeed(for: account, feed: feed, container: folder) { result in + group.leave() + switch result { + case .success: + break + case .failure(let error): + os_log(.error, log: self.log, "Restore folder feed error: %@.", error.localizedDescription) + } + } + + } + + group.notify(queue: .main) { + account.addFolder(folder) + completion(.success(())) + } } func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set
? { From 3089f2332e0c65e9417ef47a16910a079ac102ce Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Wed, 9 Oct 2019 19:47:15 +1100 Subject: [PATCH 09/16] Show progress while fetching the content of a newly created feed. --- Frameworks/Account/Feedly/FeedlyAccountDelegate.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 34e596440..591155b60 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -254,7 +254,7 @@ final class FeedlyAccountDelegate: AccountDelegate { progress.addToNumberOfTasksAndRemaining(1) caller.addFeed(with: resourceId, title: name, toCollectionWith: collectionId) { [weak self] result in - progress.completeTask() + defer { progress.completeTask() } switch result { case .success(let feedlyFeeds): @@ -276,6 +276,7 @@ final class FeedlyAccountDelegate: AccountDelegate { } let group = DispatchGroup() + progress.addToNumberOfTasksAndRemaining(1) if let self = self { for feed in added { @@ -299,6 +300,7 @@ final class FeedlyAccountDelegate: AccountDelegate { } group.notify(queue: .main) { + progress.completeTask() completion(.success(first)) } From 073ad077671b1c94ba4970226d8d05d1218cdbc4 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 9 Oct 2019 02:47:34 -0400 Subject: [PATCH 10/16] Use ORGANIZATION_IDENTIFIER for the bundle identifier The ORGANIZATION_IDENTIFIER was not being used on the Mac and Safari Extension. This made the bundle and provisioning profile be com.ranchero.XYZ, which now conflicts because of added entitlements and signing. Changing this to use the ORGANIZATION_IDENTIFIER lets developers have a unique ID --- xcconfig/NetNewsWire_macapp_target.xcconfig | 2 +- xcconfig/NetNewsWire_safariextension_target.xcconfig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/xcconfig/NetNewsWire_macapp_target.xcconfig b/xcconfig/NetNewsWire_macapp_target.xcconfig index f24b25dbf..fa302be5f 100644 --- a/xcconfig/NetNewsWire_macapp_target.xcconfig +++ b/xcconfig/NetNewsWire_macapp_target.xcconfig @@ -37,6 +37,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon CODE_SIGN_ENTITLEMENTS = Mac/Resources/NetNewsWire.entitlements INFOPLIST_FILE = Mac/Resources/Info.plist LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks -PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.NetNewsWire-Evergreen +PRODUCT_BUNDLE_IDENTIFIER = $(ORGANIZATION_IDENTIFIER).NetNewsWire-Evergreen PRODUCT_NAME = NetNewsWire SWIFT_OBJC_BRIDGING_HEADER = Mac/NetNewsWire-Bridging-Header.h diff --git a/xcconfig/NetNewsWire_safariextension_target.xcconfig b/xcconfig/NetNewsWire_safariextension_target.xcconfig index d1cc16acd..d77bb41e7 100644 --- a/xcconfig/NetNewsWire_safariextension_target.xcconfig +++ b/xcconfig/NetNewsWire_safariextension_target.xcconfig @@ -34,7 +34,7 @@ PROVISIONING_PROFILE_SPECIFIER = CODE_SIGN_ENTITLEMENTS = Mac/SafariExtension/Subscribe_to_Feed.entitlements INFOPLIST_FILE = Mac/SafariExtension/Info.plist LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks -PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.NetNewsWire-Evergreen.Subscribe-to-Feed +PRODUCT_BUNDLE_IDENTIFIER = $(ORGANIZATION_IDENTIFIER).NetNewsWire-Evergreen.Subscribe-to-Feed PRODUCT_NAME = $(TARGET_NAME) SDKROOT = macosx From d183f3672d729e6ed7fb12781233c6c3b7d01545 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 9 Oct 2019 10:23:58 -0500 Subject: [PATCH 11/16] Ported sub/sup rules over from Mac stylesheet --- iOS/Resources/styleSheet.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/iOS/Resources/styleSheet.css b/iOS/Resources/styleSheet.css index 6405a9347..df1c80612 100644 --- a/iOS/Resources/styleSheet.css +++ b/iOS/Resources/styleSheet.css @@ -141,6 +141,18 @@ figcaption { line-height: 1.3em; } +sup { + vertical-align: top; + position: relative; + bottom: 0.2rem; +} + +sub { + vertical-align: bottom; + position: relative; + top: 0.2rem; +} + .iframeWrap { position: relative; display: block; From 742796aaa8e5c4aab7c2f58bd2a2816fb0491174 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 9 Oct 2019 10:24:30 -0500 Subject: [PATCH 12/16] Add default organization identifier to xcconfig files --- xcconfig/NetNewsWire_macapp_target.xcconfig | 1 + xcconfig/NetNewsWire_safariextension_target.xcconfig | 1 + 2 files changed, 2 insertions(+) diff --git a/xcconfig/NetNewsWire_macapp_target.xcconfig b/xcconfig/NetNewsWire_macapp_target.xcconfig index fa302be5f..9065dd1b6 100644 --- a/xcconfig/NetNewsWire_macapp_target.xcconfig +++ b/xcconfig/NetNewsWire_macapp_target.xcconfig @@ -1,6 +1,7 @@ CODE_SIGN_IDENTITY = Mac Developer DEVELOPMENT_TEAM = M8L2WTLA8W CODE_SIGN_STYLE = Automatic +ORGANIZATION_IDENTIFIER = com.ranchero PROVISIONING_PROFILE_SPECIFIER = // developers can locally override the Xcode settings for code signing diff --git a/xcconfig/NetNewsWire_safariextension_target.xcconfig b/xcconfig/NetNewsWire_safariextension_target.xcconfig index d77bb41e7..6c536de2b 100644 --- a/xcconfig/NetNewsWire_safariextension_target.xcconfig +++ b/xcconfig/NetNewsWire_safariextension_target.xcconfig @@ -1,6 +1,7 @@ CODE_SIGN_IDENTITY = Mac Developer DEVELOPMENT_TEAM = M8L2WTLA8W CODE_SIGN_STYLE = Automatic +ORGANIZATION_IDENTIFIER = com.ranchero PROVISIONING_PROFILE_SPECIFIER = // developers can locally override the Xcode settings for code signing From 411ed1855168cdced136f961f27c2ce56549ce45 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 9 Oct 2019 10:37:52 -0500 Subject: [PATCH 13/16] Add org identifier documentation --- xcconfig/NetNewsWire_safariextension_target.xcconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/xcconfig/NetNewsWire_safariextension_target.xcconfig b/xcconfig/NetNewsWire_safariextension_target.xcconfig index 6c536de2b..9f854fbec 100644 --- a/xcconfig/NetNewsWire_safariextension_target.xcconfig +++ b/xcconfig/NetNewsWire_safariextension_target.xcconfig @@ -17,6 +17,7 @@ PROVISIONING_PROFILE_SPECIFIER = // CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer // CODE_SIGN_IDENTITY[sdk=iphonesimulator*] = iPhone Developer // DEVELOPMENT_TEAM = +// ORGANIZATION_IDENTIFIER = // CODE_SIGN_STYLE = Automatic // PROVISIONING_PROFILE_SPECIFIER = // From b52c67595f0584047636065fbab270c62e0fbb3b Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 9 Oct 2019 11:45:36 -0500 Subject: [PATCH 14/16] Add show/hide button to password on add accounts and credentials update. Issue #1066 --- NetNewsWire.xcodeproj/project.pbxproj | 12 +++++ .../Account/SettingsFeedbinAccountView.swift | 2 +- iOS/SwiftUI Extensions/PasswordField.swift | 25 +++++++++ .../ShowHidePasswordView.swift | 51 +++++++++++++++++++ .../ShowHidePasswordView.xib | 46 +++++++++++++++++ 5 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 iOS/SwiftUI Extensions/PasswordField.swift create mode 100644 iOS/SwiftUI Extensions/ShowHidePasswordView.swift create mode 100644 iOS/SwiftUI Extensions/ShowHidePasswordView.xib diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 7792cc046..fab3da656 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -184,6 +184,9 @@ 51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; 51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */; }; 51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; }; + 51E149B3234D82E40004F7A5 /* PasswordField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E149B2234D82E40004F7A5 /* PasswordField.swift */; }; + 51E149C0234D839E0004F7A5 /* ShowHidePasswordView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51E149BF234D839E0004F7A5 /* ShowHidePasswordView.xib */; }; + 51E149C2234D852F0004F7A5 /* ShowHidePasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E149C1234D852F0004F7A5 /* ShowHidePasswordView.swift */; }; 51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; }; 51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB3C229AB08300645299 /* ErrorHandler.swift */; }; 51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; }; @@ -802,6 +805,9 @@ 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = ""; }; 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = ""; }; 51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = ""; }; + 51E149B2234D82E40004F7A5 /* PasswordField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordField.swift; sourceTree = ""; }; + 51E149BF234D839E0004F7A5 /* ShowHidePasswordView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowHidePasswordView.xib; sourceTree = ""; }; + 51E149C1234D852F0004F7A5 /* ShowHidePasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowHidePasswordView.swift; sourceTree = ""; }; 51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; 51E3EB3C229AB08300645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleStatusSyncTimer.swift; sourceTree = ""; }; @@ -1244,6 +1250,9 @@ DF999FF622B5AEFA0064B687 /* SafariView.swift */, 51322858232FDDB80033D4ED /* VibrantButtonStyle.swift */, 51322854232EED360033D4ED /* VibrantSelectAction.swift */, + 51E149B2234D82E40004F7A5 /* PasswordField.swift */, + 51E149C1234D852F0004F7A5 /* ShowHidePasswordView.swift */, + 51E149BF234D839E0004F7A5 /* ShowHidePasswordView.xib */, ); path = "SwiftUI Extensions"; sourceTree = ""; @@ -2481,6 +2490,7 @@ 51F85BED227251DF00C787DC /* Acknowledgments.rtf in Resources */, 511D43D1231FA62800FB1562 /* SidebarKeyboardShortcuts.plist in Resources */, 51C452AB22650DC600C03939 /* template.html in Resources */, + 51E149C0234D839E0004F7A5 /* ShowHidePasswordView.xib in Resources */, 51F85BF12272524100C787DC /* Credits.rtf in Resources */, 84A3EE61223B667F00557320 /* DefaultFeeds.opml in Resources */, 511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */, @@ -2709,6 +2719,7 @@ buildActionMask = 2147483647; files = ( 840D617F2029031C009BC708 /* AppDelegate.swift in Sources */, + 51E149B3234D82E40004F7A5 /* PasswordField.swift in Sources */, 512E08E72268801200BDCFDD /* FeedTreeControllerDelegate.swift in Sources */, 51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */, 51EF0F79227716380050506E /* ColorHash.swift in Sources */, @@ -2777,6 +2788,7 @@ 51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */, 51C4527F2265092C00C03939 /* ArticleViewController.swift in Sources */, 51C4526A226508F600C03939 /* MasterFeedTableViewCellLayout.swift in Sources */, + 51E149C2234D852F0004F7A5 /* ShowHidePasswordView.swift in Sources */, 51C452AE2265104D00C03939 /* TimelineStringFormatter.swift in Sources */, 512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */, 51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */, diff --git a/iOS/Settings/Account/SettingsFeedbinAccountView.swift b/iOS/Settings/Account/SettingsFeedbinAccountView.swift index 511318a81..a685a8ada 100644 --- a/iOS/Settings/Account/SettingsFeedbinAccountView.swift +++ b/iOS/Settings/Account/SettingsFeedbinAccountView.swift @@ -31,7 +31,7 @@ struct SettingsFeedbinAccountView : View { TextField("Email", text: $viewModel.email) .keyboardType(.emailAddress) .textContentType(.emailAddress) - SecureField("Password", text: $viewModel.password) + PasswordField(password: $viewModel.password) } Section(footer: HStack { diff --git a/iOS/SwiftUI Extensions/PasswordField.swift b/iOS/SwiftUI Extensions/PasswordField.swift new file mode 100644 index 000000000..8875837ae --- /dev/null +++ b/iOS/SwiftUI Extensions/PasswordField.swift @@ -0,0 +1,25 @@ +// +// PasswordField.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 10/8/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import SwiftUI + +struct PasswordField: UIViewRepresentable { + + let password: Binding + + func makeUIView(context: Context) -> ShowHidePasswordView { + let showHideView = Bundle.main.loadNibNamed("ShowHidePasswordView", owner: Self.self, options: nil)?[0] as! ShowHidePasswordView + showHideView.passwordTextField.bindingString = password + return showHideView + } + + func updateUIView(_ showHideView: ShowHidePasswordView, context: Context) { + showHideView.passwordTextField.bindingString = password + } + +} diff --git a/iOS/SwiftUI Extensions/ShowHidePasswordView.swift b/iOS/SwiftUI Extensions/ShowHidePasswordView.swift new file mode 100644 index 000000000..fe6b10d02 --- /dev/null +++ b/iOS/SwiftUI Extensions/ShowHidePasswordView.swift @@ -0,0 +1,51 @@ +// +// ShowHidePasswordView.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 10/8/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit +import SwiftUI + +class ShowHidePasswordView: UIView { + + @IBOutlet weak var passwordTextField: BindingTextField! + @IBOutlet weak var showHideButton: UIButton! + + @IBAction func toggleShowHideButton(_ sender: Any) { + if passwordTextField.isSecureTextEntry { + passwordTextField.isSecureTextEntry = false + showHideButton.setTitle(NSLocalizedString("Hide", comment: "Hide"), for: .normal) + } else { + passwordTextField.isSecureTextEntry = true + showHideButton.setTitle(NSLocalizedString("Show", comment: "Show"), for: .normal) + } + } + +} + +class BindingTextField: UITextField, UITextFieldDelegate { + + var bindingString: Binding? = nil + + override init(frame: CGRect) { + super.init(frame: frame) + delegate = self + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + delegate = self + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if let currentValue = textField.text as NSString? { + let proposedValue = currentValue.replacingCharacters(in: range, with: string) + bindingString?.wrappedValue = proposedValue + } + return true + } + +} diff --git a/iOS/SwiftUI Extensions/ShowHidePasswordView.xib b/iOS/SwiftUI Extensions/ShowHidePasswordView.xib new file mode 100644 index 000000000..d68686200 --- /dev/null +++ b/iOS/SwiftUI Extensions/ShowHidePasswordView.xib @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c2f29c90859e1123f21a8e909f13008843fd5a43 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 9 Oct 2019 11:55:14 -0500 Subject: [PATCH 15/16] Remove some team specific properties --- NetNewsWire.xcodeproj/project.pbxproj | 8 -------- 1 file changed, 8 deletions(-) diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index fab3da656..3e78283af 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -2215,15 +2215,11 @@ TargetAttributes = { 513C5CE5232571C2003D4054 = { CreatedOnToolsVersion = 11.0; - DevelopmentTeam = M8L2WTLA8W; - ProvisioningStyle = Automatic; }; 6581C73220CED60000F4AD34 = { }; 840D617B2029031C009BC708 = { CreatedOnToolsVersion = 9.3; - DevelopmentTeam = M8L2WTLA8W; - ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.BackgroundModes = { enabled = 1; @@ -2232,8 +2228,6 @@ }; 849C645F1ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = M8L2WTLA8W; - ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.HardenedRuntime = { enabled = 1; @@ -2242,8 +2236,6 @@ }; 849C64701ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = 9C84TZ7Q6Z; - ProvisioningStyle = Automatic; TestTargetID = 849C645F1ED37A5D003D8FC0; }; }; From d80b83eaf39f11e6ab4051eda8af3b45c33b5678 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 9 Oct 2019 16:36:06 -0500 Subject: [PATCH 16/16] Updates to do manual provisioning --- buildscripts/ci-build.sh | 20 +++++++++++++++---- xcconfig/NetNewsWire_macapp_target.xcconfig | 4 ++-- ...etNewsWire_safariextension_target.xcconfig | 4 ++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/buildscripts/ci-build.sh b/buildscripts/ci-build.sh index f1d5cc9ef..c33c17834 100755 --- a/buildscripts/ci-build.sh +++ b/buildscripts/ci-build.sh @@ -2,9 +2,12 @@ set -v set -e +# Unencrypt our provisioning profile, certificate, and private key +openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in buildscripts/profile/NetNewsWire.provisionprofile.enc -d -a -out buildscripts/profile/NetNewsWire.provisionprofile openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in buildscripts/certs/dev.cer.enc -d -a -out buildscripts/certs/dev.cer openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in buildscripts/certs/dev.p12.enc -d -a -out buildscripts/certs/dev.p12 +# Put the certificates and private key in the Keychain, set ACL permissions, and make default security create-keychain -p github-actions github-build.keychain security import buildscripts/certs/apple.cer -k ~/Library/Keychains/github-build.keychain -A security import buildscripts/certs/dev.cer -k ~/Library/Keychains/github-build.keychain -A @@ -12,9 +15,18 @@ security import buildscripts/certs/dev.p12 -k ~/Library/Keychains/github-build.k security set-key-partition-list -S apple-tool:,apple: -s -k github-actions github-build.keychain security default-keychain -s github-build.keychain -rm -f ./buildscripts/certs/dev.cer -rm -f ./buildscripts/certs/dev.p12 +# Copy the provisioning profile +mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles +cp buildscripts/profile/NetNewsWire.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/ -xcodebuild -scheme 'NetNewsWire' -configuration Debug -allowProvisioningUpdates -showBuildTimingSummary +# Delete the decrypted files +rm -f buildscripts/profile/NetNewsWire.provisionprofile +rm -f buildscripts/certs/dev.cer +rm -f buildscripts/certs/dev.p12 -security delete-keychain github-build.keychain \ No newline at end of file +# Do the build +xcodebuild -scheme 'NetNewsWire' -configuration Release -showBuildTimingSummary + +# Delete the keychain and the provisioningi profile +security delete-keychain github-build.keychain +rm -f ~/Library/MobileDevice/Provisioning\ Profiles/NetNewsWire.provisionprofile \ No newline at end of file diff --git a/xcconfig/NetNewsWire_macapp_target.xcconfig b/xcconfig/NetNewsWire_macapp_target.xcconfig index 9065dd1b6..c3500d89c 100644 --- a/xcconfig/NetNewsWire_macapp_target.xcconfig +++ b/xcconfig/NetNewsWire_macapp_target.xcconfig @@ -1,6 +1,6 @@ -CODE_SIGN_IDENTITY = Mac Developer +CODE_SIGN_IDENTITY = Developer ID Application DEVELOPMENT_TEAM = M8L2WTLA8W -CODE_SIGN_STYLE = Automatic +CODE_SIGN_STYLE = Manual ORGANIZATION_IDENTIFIER = com.ranchero PROVISIONING_PROFILE_SPECIFIER = diff --git a/xcconfig/NetNewsWire_safariextension_target.xcconfig b/xcconfig/NetNewsWire_safariextension_target.xcconfig index 9f854fbec..8caead968 100644 --- a/xcconfig/NetNewsWire_safariextension_target.xcconfig +++ b/xcconfig/NetNewsWire_safariextension_target.xcconfig @@ -1,6 +1,6 @@ -CODE_SIGN_IDENTITY = Mac Developer +CODE_SIGN_IDENTITY = Developer ID Application DEVELOPMENT_TEAM = M8L2WTLA8W -CODE_SIGN_STYLE = Automatic +CODE_SIGN_STYLE = Manual ORGANIZATION_IDENTIFIER = com.ranchero PROVISIONING_PROFILE_SPECIFIER =