Merge branch 'master' into accent-color-experimental
This commit is contained in:
commit
a8d3872490
|
@ -243,6 +243,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport, api: FeedlyAccountDelegate.environment)
|
||||
case .feedWrangler:
|
||||
self.delegate = FeedWranglerAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
case .newsBlur:
|
||||
self.delegate = NewsBlurAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
@ -325,6 +327,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
ReaderAPIAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, completion: completion)
|
||||
case .feedWrangler:
|
||||
FeedWranglerAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
||||
case .newsBlur:
|
||||
NewsBlurAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
@ -7,6 +7,16 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB7399814F6FB3247825C /* NewsBlurStory.swift */; };
|
||||
179DB0B17A6C51B95ABC1741 /* NewsBlurStoryStatusChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB5B421C5433B45C5F13E /* NewsBlurStoryStatusChange.swift */; };
|
||||
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */; };
|
||||
179DB3A93E3205EF29C2AF62 /* NewsBlurAPICaller+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DBBF346CF712AB2F0E9E6 /* NewsBlurAPICaller+Internal.swift */; };
|
||||
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */; };
|
||||
179DBCB4B11C88EBE852A015 /* NewsBlurFeedChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB3CBADAFCF5377DA3D02 /* NewsBlurFeedChange.swift */; };
|
||||
179DBD4ECC1C9712DF51DB8C /* NewsBlurFolderChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DBDDC00B68411AA28941F /* NewsBlurFolderChange.swift */; };
|
||||
179DBE829FDF48E102F73244 /* NewsBlurAccountDelegate+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB78C47256A122A281942 /* NewsBlurAccountDelegate+Internal.swift */; };
|
||||
179DBED55C9B4D6A413486C1 /* NewsBlurStoryHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB818180A51098A9816B2 /* NewsBlurStoryHash.swift */; };
|
||||
179DBF4DE2562D4C532F6008 /* NewsBlurFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB1B909672E0E807B5E8C /* NewsBlurFeed.swift */; };
|
||||
3B3A33E7238D3D6800314204 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3A33E6238D3D6800314204 /* Secrets.swift */; };
|
||||
3B826DA72385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826D9E2385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift */; };
|
||||
3B826DA82385C81C00FC1ADB /* FeedWranglerFeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826D9F2385C81C00FC1ADB /* FeedWranglerFeedItem.swift */; };
|
||||
|
@ -18,7 +28,7 @@
|
|||
3B826DAE2385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DA52385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift */; };
|
||||
3B826DAF2385C81C00FC1ADB /* FeedWranglerGenericResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DA62385C81C00FC1ADB /* FeedWranglerGenericResult.swift */; };
|
||||
3BC23AB92385ECB100371CBA /* FeedWranglerSubscriptionResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC23AB82385ECB100371CBA /* FeedWranglerSubscriptionResult.swift */; };
|
||||
5103A9D92422546800410853 /* CloudKitAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103A9D82422546800410853 /* CloudKitAppDelegate.swift */; };
|
||||
5103A9D92422546800410853 /* CloudKitAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */; };
|
||||
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; };
|
||||
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; };
|
||||
510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD110232C3801002692E4 /* AccountMetadataFile.swift */; };
|
||||
|
@ -63,6 +73,8 @@
|
|||
552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F2229D5D5A009559E0 /* ReaderAPITagging.swift */; };
|
||||
552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F3229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift */; };
|
||||
55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F5229D5D5A009559E0 /* ReaderAPICaller.swift */; };
|
||||
769F295938E5A30D03DFF88F /* NewsBlurAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 769F2A8DF190549E24B5D110 /* NewsBlurAccountDelegate.swift */; };
|
||||
769F2BA02EF5F329CDE45F5A /* NewsBlurAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 769F275FD5D942502C5B4716 /* NewsBlurAPICaller.swift */; };
|
||||
841973FE1F6DD1BC006346C4 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973EF1F6DD19E006346C4 /* RSCore.framework */; };
|
||||
841973FF1F6DD1C5006346C4 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973FA1F6DD1AC006346C4 /* RSParser.framework */; };
|
||||
841974011F6DD1EC006346C4 /* Folder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841974001F6DD1EC006346C4 /* Folder.swift */; };
|
||||
|
@ -220,6 +232,16 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurLoginResponse.swift; sourceTree = "<group>"; };
|
||||
179DB1B909672E0E807B5E8C /* NewsBlurFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurFeed.swift; sourceTree = "<group>"; };
|
||||
179DB3CBADAFCF5377DA3D02 /* NewsBlurFeedChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurFeedChange.swift; sourceTree = "<group>"; };
|
||||
179DB5B421C5433B45C5F13E /* NewsBlurStoryStatusChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurStoryStatusChange.swift; sourceTree = "<group>"; };
|
||||
179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurGenericCodingKeys.swift; sourceTree = "<group>"; };
|
||||
179DB7399814F6FB3247825C /* NewsBlurStory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurStory.swift; sourceTree = "<group>"; };
|
||||
179DB78C47256A122A281942 /* NewsBlurAccountDelegate+Internal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NewsBlurAccountDelegate+Internal.swift"; sourceTree = "<group>"; };
|
||||
179DB818180A51098A9816B2 /* NewsBlurStoryHash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurStoryHash.swift; sourceTree = "<group>"; };
|
||||
179DBBF346CF712AB2F0E9E6 /* NewsBlurAPICaller+Internal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NewsBlurAPICaller+Internal.swift"; sourceTree = "<group>"; };
|
||||
179DBDDC00B68411AA28941F /* NewsBlurFolderChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurFolderChange.swift; sourceTree = "<group>"; };
|
||||
3B3A33E6238D3D6800314204 /* Secrets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Secrets.swift; path = ../../Shared/Secrets.swift; sourceTree = "<group>"; };
|
||||
3B826D9E2385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAuthorizationResult.swift; sourceTree = "<group>"; };
|
||||
3B826D9F2385C81C00FC1ADB /* FeedWranglerFeedItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerFeedItem.swift; sourceTree = "<group>"; };
|
||||
|
@ -231,7 +253,7 @@
|
|||
3B826DA52385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerSubscriptionsRequest.swift; sourceTree = "<group>"; };
|
||||
3B826DA62385C81C00FC1ADB /* FeedWranglerGenericResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerGenericResult.swift; sourceTree = "<group>"; };
|
||||
3BC23AB82385ECB100371CBA /* FeedWranglerSubscriptionResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedWranglerSubscriptionResult.swift; sourceTree = "<group>"; };
|
||||
5103A9D82422546800410853 /* CloudKitAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAppDelegate.swift; sourceTree = "<group>"; };
|
||||
5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAccountDelegate.swift; sourceTree = "<group>"; };
|
||||
5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCredentialsTest.swift; sourceTree = "<group>"; };
|
||||
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = "<group>"; };
|
||||
5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; };
|
||||
|
@ -278,6 +300,8 @@
|
|||
552032F2229D5D5A009559E0 /* ReaderAPITagging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITagging.swift; sourceTree = "<group>"; };
|
||||
552032F3229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIAccountDelegate.swift; sourceTree = "<group>"; };
|
||||
552032F5229D5D5A009559E0 /* ReaderAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPICaller.swift; sourceTree = "<group>"; };
|
||||
769F275FD5D942502C5B4716 /* NewsBlurAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurAPICaller.swift; sourceTree = "<group>"; };
|
||||
769F2A8DF190549E24B5D110 /* NewsBlurAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurAccountDelegate.swift; sourceTree = "<group>"; };
|
||||
841973E81F6DD19E006346C4 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = "<group>"; };
|
||||
841973F41F6DD1AC006346C4 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; };
|
||||
841974001F6DD1EC006346C4 /* Folder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folder.swift; sourceTree = "<group>"; };
|
||||
|
@ -435,6 +459,30 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
179DB1571B95BAD0F833AF6D /* Internals */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
179DB78C47256A122A281942 /* NewsBlurAccountDelegate+Internal.swift */,
|
||||
179DBBF346CF712AB2F0E9E6 /* NewsBlurAPICaller+Internal.swift */,
|
||||
);
|
||||
path = Internals;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
179DBD810D353D9CED7C3BED /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */,
|
||||
179DB1B909672E0E807B5E8C /* NewsBlurFeed.swift */,
|
||||
179DB7399814F6FB3247825C /* NewsBlurStory.swift */,
|
||||
179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */,
|
||||
179DB818180A51098A9816B2 /* NewsBlurStoryHash.swift */,
|
||||
179DB5B421C5433B45C5F13E /* NewsBlurStoryStatusChange.swift */,
|
||||
179DBDDC00B68411AA28941F /* NewsBlurFolderChange.swift */,
|
||||
179DB3CBADAFCF5377DA3D02 /* NewsBlurFeedChange.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3B826D9D2385C81C00FC1ADB /* FeedWrangler */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -455,7 +503,7 @@
|
|||
5103A9D7242253DC00410853 /* CloudKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5103A9D82422546800410853 /* CloudKitAppDelegate.swift */,
|
||||
5103A9D82422546800410853 /* CloudKitAccountDelegate.swift */,
|
||||
);
|
||||
path = CloudKit;
|
||||
sourceTree = "<group>";
|
||||
|
@ -523,6 +571,17 @@
|
|||
path = ReaderAPI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
769F2630AF8DC873D4A73567 /* NewsBlur */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
769F2A8DF190549E24B5D110 /* NewsBlurAccountDelegate.swift */,
|
||||
769F275FD5D942502C5B4716 /* NewsBlurAPICaller.swift */,
|
||||
179DBD810D353D9CED7C3BED /* Models */,
|
||||
179DB1571B95BAD0F833AF6D /* Internals */,
|
||||
);
|
||||
path = NewsBlur;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
841973E91F6DD19E006346C4 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -622,6 +681,7 @@
|
|||
8469F80F1F6DC3C10084783E /* Frameworks */,
|
||||
D511EEB4202422BB00712EC3 /* xcconfig */,
|
||||
848934FA1F62484F00CEBD24 /* Info.plist */,
|
||||
769F2630AF8DC873D4A73567 /* NewsBlur */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
usesTabs = 1;
|
||||
|
@ -1019,7 +1079,7 @@
|
|||
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
|
||||
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
|
||||
510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */,
|
||||
5103A9D92422546800410853 /* CloudKitAppDelegate.swift in Sources */,
|
||||
5103A9D92422546800410853 /* CloudKitAccountDelegate.swift in Sources */,
|
||||
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */,
|
||||
9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */,
|
||||
9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */,
|
||||
|
@ -1107,6 +1167,18 @@
|
|||
9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */,
|
||||
3B826DAA2385C81C00FC1ADB /* FeedWranglerSubscription.swift in Sources */,
|
||||
3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */,
|
||||
769F295938E5A30D03DFF88F /* NewsBlurAccountDelegate.swift in Sources */,
|
||||
769F2BA02EF5F329CDE45F5A /* NewsBlurAPICaller.swift in Sources */,
|
||||
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */,
|
||||
179DBF4DE2562D4C532F6008 /* NewsBlurFeed.swift in Sources */,
|
||||
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */,
|
||||
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */,
|
||||
179DBED55C9B4D6A413486C1 /* NewsBlurStoryHash.swift in Sources */,
|
||||
179DB0B17A6C51B95ABC1741 /* NewsBlurStoryStatusChange.swift in Sources */,
|
||||
179DBD4ECC1C9712DF51DB8C /* NewsBlurFolderChange.swift in Sources */,
|
||||
179DBCB4B11C88EBE852A015 /* NewsBlurFeedChange.swift in Sources */,
|
||||
179DBE829FDF48E102F73244 /* NewsBlurAccountDelegate+Internal.swift in Sources */,
|
||||
179DB3A93E3205EF29C2AF62 /* NewsBlurAPICaller+Internal.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -17,6 +17,8 @@ public enum CredentialsType: String {
|
|||
case basic = "password"
|
||||
case feedWranglerBasic = "feedWranglerBasic"
|
||||
case feedWranglerToken = "feedWranglerToken"
|
||||
case newsBlurBasic = "newsBlurBasic"
|
||||
case newsBlurSessionId = "newsBlurSessionId"
|
||||
case readerBasic = "readerBasic"
|
||||
case readerAPIKey = "readerAPIKey"
|
||||
case oauthAccessToken = "oauthAccessToken"
|
||||
|
|
|
@ -33,7 +33,19 @@ public extension URLRequest {
|
|||
])
|
||||
case .feedWranglerToken:
|
||||
self.url = url.appendingQueryItem(URLQueryItem(name: "access_token", value: credentials.secret))
|
||||
case .readerBasic:
|
||||
case .newsBlurBasic:
|
||||
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
httpMethod = "POST"
|
||||
var postData = URLComponents()
|
||||
postData.queryItems = [
|
||||
URLQueryItem(name: "username", value: credentials.username),
|
||||
URLQueryItem(name: "password", value: credentials.secret),
|
||||
]
|
||||
httpBody = postData.percentEncodedQuery?.data(using: .utf8)
|
||||
case .newsBlurSessionId:
|
||||
setValue("\(NewsBlurAPICaller.SessionIdCookie)=\(credentials.secret)", forHTTPHeaderField: "Cookie")
|
||||
httpShouldHandleCookies = true
|
||||
case .readerBasic:
|
||||
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
httpMethod = "POST"
|
||||
var postData = URLComponents()
|
||||
|
|
|
@ -493,7 +493,7 @@ private extension FeedWranglerAccountDelegate {
|
|||
feed.name = subscription.title
|
||||
feed.editedName = nil
|
||||
feed.homePageURL = subscription.siteURL
|
||||
feed.subscriptionID = nil // MARK: TODO What should this be?
|
||||
feed.externalID = nil // MARK: TODO What should this be?
|
||||
} else {
|
||||
subscriptionsToAdd.insert(subscription)
|
||||
}
|
||||
|
@ -502,7 +502,7 @@ private extension FeedWranglerAccountDelegate {
|
|||
subscriptionsToAdd.forEach { subscription in
|
||||
let feedId = String(subscription.feedID)
|
||||
let feed = account.createWebFeed(with: subscription.title, url: subscription.feedURL, webFeedID: feedId, homePageURL: subscription.siteURL)
|
||||
feed.subscriptionID = nil
|
||||
feed.externalID = nil
|
||||
account.addWebFeed(feed)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -334,7 +334,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
|
||||
} else {
|
||||
|
||||
if let subscriptionID = feed.subscriptionID {
|
||||
if let subscriptionID = feed.externalID {
|
||||
group.enter()
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
caller.deleteSubscription(subscriptionID: subscriptionID) { result in
|
||||
|
@ -398,7 +398,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// This error should never happen
|
||||
guard let subscriptionID = feed.subscriptionID else {
|
||||
guard let subscriptionID = feed.externalID else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
@ -812,7 +812,7 @@ private extension FeedbinAccountDelegate {
|
|||
// If the name has been changed on the server remove the locally edited name
|
||||
feed.editedName = nil
|
||||
feed.homePageURL = subscription.homePageURL
|
||||
feed.subscriptionID = String(subscription.subscriptionID)
|
||||
feed.externalID = String(subscription.subscriptionID)
|
||||
feed.faviconURL = subscription.jsonFeed?.favicon
|
||||
feed.iconURL = subscription.jsonFeed?.icon
|
||||
}
|
||||
|
@ -824,7 +824,7 @@ private extension FeedbinAccountDelegate {
|
|||
// Actually add subscriptions all in one go, so we don’t trigger various rebuilding things that Account does.
|
||||
subscriptionsToAdd.forEach { subscription in
|
||||
let feed = account.createWebFeed(with: subscription.name, url: subscription.url, webFeedID: String(subscription.feedID), homePageURL: subscription.homePageURL)
|
||||
feed.subscriptionID = String(subscription.subscriptionID)
|
||||
feed.externalID = String(subscription.subscriptionID)
|
||||
account.addWebFeed(feed)
|
||||
}
|
||||
}
|
||||
|
@ -1004,7 +1004,7 @@ private extension FeedbinAccountDelegate {
|
|||
DispatchQueue.main.async {
|
||||
|
||||
let feed = account.createWebFeed(with: sub.name, url: sub.url, webFeedID: String(sub.feedID), homePageURL: sub.homePageURL)
|
||||
feed.subscriptionID = String(sub.subscriptionID)
|
||||
feed.externalID = String(sub.subscriptionID)
|
||||
feed.iconURL = sub.jsonFeed?.icon
|
||||
feed.faviconURL = sub.jsonFeed?.favicon
|
||||
|
||||
|
@ -1351,7 +1351,7 @@ private extension FeedbinAccountDelegate {
|
|||
func deleteSubscription(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// This error should never happen
|
||||
guard let subscriptionID = feed.subscriptionID else {
|
||||
guard let subscriptionID = feed.externalID else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
//
|
||||
// NewsBlurAPICaller+Internal.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh Quang Do on 2020-03-21.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSWeb
|
||||
|
||||
protocol NewsBlurDataConvertible {
|
||||
var asData: Data? { get }
|
||||
}
|
||||
|
||||
enum NewsBlurError: LocalizedError {
|
||||
case general(message: String)
|
||||
case invalidParameter
|
||||
case unknown
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .general(let message):
|
||||
return message
|
||||
case .invalidParameter:
|
||||
return "There was an invalid parameter passed"
|
||||
case .unknown:
|
||||
return "An unknown error occurred"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interact with endpoints
|
||||
|
||||
extension NewsBlurAPICaller {
|
||||
// GET endpoint, discard response
|
||||
func requestData(
|
||||
endpoint: String,
|
||||
completion: @escaping (Result<Void, Error>) -> Void
|
||||
) {
|
||||
let callURL = baseURL.appendingPathComponent(endpoint)
|
||||
|
||||
requestData(callURL: callURL, completion: completion)
|
||||
}
|
||||
|
||||
// GET endpoint
|
||||
func requestData<R: Decodable>(
|
||||
endpoint: String,
|
||||
resultType: R.Type,
|
||||
dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601,
|
||||
keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
|
||||
completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void
|
||||
) {
|
||||
let callURL = baseURL.appendingPathComponent(endpoint)
|
||||
|
||||
requestData(
|
||||
callURL: callURL,
|
||||
resultType: resultType,
|
||||
dateDecoding: dateDecoding,
|
||||
keyDecoding: keyDecoding,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
// POST to endpoint, discard response
|
||||
func sendUpdates(
|
||||
endpoint: String,
|
||||
payload: NewsBlurDataConvertible,
|
||||
completion: @escaping (Result<Void, Error>) -> Void
|
||||
) {
|
||||
let callURL = baseURL.appendingPathComponent(endpoint)
|
||||
|
||||
sendUpdates(callURL: callURL, payload: payload, completion: completion)
|
||||
}
|
||||
|
||||
// POST to endpoint
|
||||
func sendUpdates<R: Decodable>(
|
||||
endpoint: String,
|
||||
payload: NewsBlurDataConvertible,
|
||||
resultType: R.Type,
|
||||
dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601,
|
||||
keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
|
||||
completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void
|
||||
) {
|
||||
let callURL = baseURL.appendingPathComponent(endpoint)
|
||||
|
||||
sendUpdates(
|
||||
callURL: callURL,
|
||||
payload: payload,
|
||||
resultType: resultType,
|
||||
dateDecoding: dateDecoding,
|
||||
keyDecoding: keyDecoding,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interact with URLs
|
||||
|
||||
extension NewsBlurAPICaller {
|
||||
// GET URL with params, discard response
|
||||
func requestData(
|
||||
callURL: URL?,
|
||||
completion: @escaping (Result<Void, Error>) -> Void
|
||||
) {
|
||||
guard let callURL = callURL else {
|
||||
completion(.failure(TransportError.noURL))
|
||||
return
|
||||
}
|
||||
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
|
||||
transport.send(request: request) { result in
|
||||
if self.suspended {
|
||||
completion(.failure(TransportError.suspended))
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GET URL with params
|
||||
func requestData<R: Decodable>(
|
||||
callURL: URL?,
|
||||
resultType: R.Type,
|
||||
dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601,
|
||||
keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
|
||||
completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void
|
||||
) {
|
||||
guard let callURL = callURL else {
|
||||
completion(.failure(TransportError.noURL))
|
||||
return
|
||||
}
|
||||
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
|
||||
transport.send(
|
||||
request: request,
|
||||
resultType: resultType,
|
||||
dateDecoding: dateDecoding,
|
||||
keyDecoding: keyDecoding
|
||||
) { result in
|
||||
if self.suspended {
|
||||
completion(.failure(TransportError.suspended))
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let response):
|
||||
completion(.success(response))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// POST to URL with params, discard response
|
||||
func sendUpdates(
|
||||
callURL: URL?,
|
||||
payload: NewsBlurDataConvertible,
|
||||
completion: @escaping (Result<Void, Error>) -> Void
|
||||
) {
|
||||
guard let callURL = callURL else {
|
||||
completion(.failure(TransportError.noURL))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.httpBody = payload.asData
|
||||
|
||||
transport.send(request: request, method: HTTPMethod.post) { result in
|
||||
if self.suspended {
|
||||
completion(.failure(TransportError.suspended))
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// POST to URL with params
|
||||
func sendUpdates<R: Decodable>(
|
||||
callURL: URL?,
|
||||
payload: NewsBlurDataConvertible,
|
||||
resultType: R.Type,
|
||||
dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601,
|
||||
keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
|
||||
completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void
|
||||
) {
|
||||
guard let callURL = callURL else {
|
||||
completion(.failure(TransportError.noURL))
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = payload.asData else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
|
||||
transport.send(
|
||||
request: request,
|
||||
method: HTTPMethod.post,
|
||||
data: data,
|
||||
resultType: resultType,
|
||||
dateDecoding: dateDecoding,
|
||||
keyDecoding: keyDecoding
|
||||
) { result in
|
||||
if self.suspended {
|
||||
completion(.failure(TransportError.suspended))
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let response):
|
||||
completion(.success(response))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,519 @@
|
|||
//
|
||||
// NewsBlurAccountDelegate+Internal.swift
|
||||
// Mostly adapted from FeedbinAccountDelegate.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh Quang Do on 2020-03-14.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Articles
|
||||
import RSCore
|
||||
import RSDatabase
|
||||
import RSParser
|
||||
import RSWeb
|
||||
import SyncDatabase
|
||||
import os.log
|
||||
|
||||
extension NewsBlurAccountDelegate {
|
||||
func refreshFeeds(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
os_log(.debug, log: log, "Refreshing feeds...")
|
||||
|
||||
caller.retrieveFeeds { result in
|
||||
switch result {
|
||||
case .success((let feeds, let folders)):
|
||||
BatchUpdate.shared.perform {
|
||||
self.syncFolders(account, folders)
|
||||
self.syncFeeds(account, feeds)
|
||||
self.syncFeedFolderRelationship(account, folders)
|
||||
}
|
||||
|
||||
self.refreshProgress.completeTask()
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncFolders(_ account: Account, _ folders: [NewsBlurFolder]?) {
|
||||
guard let folders = folders else { return }
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
os_log(.debug, log: log, "Syncing folders with %ld folders.", folders.count)
|
||||
|
||||
let folderNames = folders.map { $0.name }
|
||||
|
||||
// Delete any folders not at NewsBlur
|
||||
if let folders = account.folders {
|
||||
folders.forEach { folder in
|
||||
if !folderNames.contains(folder.name ?? "") {
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
account.addWebFeed(feed)
|
||||
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
}
|
||||
account.removeFolder(folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let accountFolderNames: [String] = {
|
||||
if let folders = account.folders {
|
||||
return folders.map { $0.name ?? "" }
|
||||
} else {
|
||||
return [String]()
|
||||
}
|
||||
}()
|
||||
|
||||
// Make any folders NewsBlur has, but we don't
|
||||
// Ignore account-level folder
|
||||
folderNames.forEach { folderName in
|
||||
if !accountFolderNames.contains(folderName) && folderName != " " {
|
||||
_ = account.ensureFolder(with: folderName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncFeeds(_ account: Account, _ feeds: [NewsBlurFeed]?) {
|
||||
guard let feeds = feeds else { return }
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
os_log(.debug, log: log, "Syncing feeds with %ld feeds.", feeds.count)
|
||||
|
||||
let newsBlurFeedIds = feeds.map { String($0.feedID) }
|
||||
|
||||
// Remove any feeds that are no longer in the subscriptions
|
||||
if let folders = account.folders {
|
||||
for folder in folders {
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
if !newsBlurFeedIds.contains(feed.webFeedID) {
|
||||
folder.removeWebFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for feed in account.topLevelWebFeeds {
|
||||
if !newsBlurFeedIds.contains(feed.webFeedID) {
|
||||
account.removeWebFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
// Add any feeds we don't have and update any we do
|
||||
var feedsToAdd = Set<NewsBlurFeed>()
|
||||
feeds.forEach { feed in
|
||||
let subFeedId = String(feed.feedID)
|
||||
|
||||
if let webFeed = account.existingWebFeed(withWebFeedID: subFeedId) {
|
||||
webFeed.name = feed.name
|
||||
// If the name has been changed on the server remove the locally edited name
|
||||
webFeed.editedName = nil
|
||||
webFeed.homePageURL = feed.homePageURL
|
||||
webFeed.externalID = String(feed.feedID)
|
||||
webFeed.faviconURL = feed.faviconURL
|
||||
}
|
||||
else {
|
||||
feedsToAdd.insert(feed)
|
||||
}
|
||||
}
|
||||
|
||||
// Actually add feeds all in one go, so we don’t trigger various rebuilding things that Account does.
|
||||
feedsToAdd.forEach { feed in
|
||||
let webFeed = account.createWebFeed(with: feed.name, url: feed.feedURL, webFeedID: String(feed.feedID), homePageURL: feed.homePageURL)
|
||||
webFeed.externalID = String(feed.feedID)
|
||||
account.addWebFeed(webFeed)
|
||||
}
|
||||
}
|
||||
|
||||
func syncFeedFolderRelationship(_ account: Account, _ folders: [NewsBlurFolder]?) {
|
||||
guard let folders = folders else { return }
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
os_log(.debug, log: log, "Syncing folders with %ld folders.", folders.count)
|
||||
|
||||
// Set up some structures to make syncing easier
|
||||
let relationships = folders.map({ $0.asRelationships }).flatMap { $0 }
|
||||
let folderDict = nameToFolderDictionary(with: account.folders)
|
||||
let newsBlurFolderDict = relationships.reduce([String: [NewsBlurFolderRelationship]]()) { (dict, relationship) in
|
||||
var feedInFolders = dict
|
||||
if var feedInFolder = feedInFolders[relationship.folderName] {
|
||||
feedInFolder.append(relationship)
|
||||
feedInFolders[relationship.folderName] = feedInFolder
|
||||
} else {
|
||||
feedInFolders[relationship.folderName] = [relationship]
|
||||
}
|
||||
return feedInFolders
|
||||
}
|
||||
|
||||
// Sync the folders
|
||||
for (folderName, folderRelationships) in newsBlurFolderDict {
|
||||
let newsBlurFolderFeedIDs = folderRelationships.map { String($0.feedID) }
|
||||
|
||||
// Handle account-level folder
|
||||
if folderName == " " {
|
||||
for feed in account.topLevelWebFeeds {
|
||||
if !newsBlurFolderFeedIDs.contains(feed.webFeedID) {
|
||||
account.removeWebFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let folder = folderDict[folderName] else { return }
|
||||
|
||||
// Move any feeds not in the folder to the account
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
if !newsBlurFolderFeedIDs.contains(feed.webFeedID) {
|
||||
folder.removeWebFeed(feed)
|
||||
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
account.addWebFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
// Add any feeds not in the folder
|
||||
let folderFeedIds = folder.topLevelWebFeeds.map { $0.webFeedID }
|
||||
|
||||
for relationship in folderRelationships {
|
||||
let folderFeedID = String(relationship.feedID)
|
||||
if !folderFeedIds.contains(folderFeedID) {
|
||||
guard let feed = account.existingWebFeed(withWebFeedID: folderFeedID) else {
|
||||
continue
|
||||
}
|
||||
saveFolderRelationship(for: feed, withFolderName: folderName, id: relationship.folderName)
|
||||
folder.addWebFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clearFolderRelationship(for feed: WebFeed, withFolderName folderName: String) {
|
||||
if var folderRelationship = feed.folderRelationship {
|
||||
folderRelationship[folderName] = nil
|
||||
feed.folderRelationship = folderRelationship
|
||||
}
|
||||
}
|
||||
|
||||
func saveFolderRelationship(for feed: WebFeed, withFolderName folderName: String, id: String) {
|
||||
if var folderRelationship = feed.folderRelationship {
|
||||
folderRelationship[folderName] = id
|
||||
feed.folderRelationship = folderRelationship
|
||||
} else {
|
||||
feed.folderRelationship = [folderName: id]
|
||||
}
|
||||
}
|
||||
|
||||
func nameToFolderDictionary(with folders: Set<Folder>?) -> [String: Folder] {
|
||||
guard let folders = folders else {
|
||||
return [String: Folder]()
|
||||
}
|
||||
|
||||
var d = [String: Folder]()
|
||||
for folder in folders {
|
||||
let name = folder.name ?? ""
|
||||
if d[name] == nil {
|
||||
d[name] = folder
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func refreshUnreadStories(for account: Account, hashes: [NewsBlurStoryHash]?, updateFetchDate: Date?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let hashes = hashes, !hashes.isEmpty else {
|
||||
if let lastArticleFetch = updateFetchDate {
|
||||
self.accountMetadata?.lastArticleFetchStartTime = lastArticleFetch
|
||||
self.accountMetadata?.lastArticleFetchEndTime = Date()
|
||||
}
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
let numberOfStories = min(hashes.count, 100) // api limit
|
||||
let hashesToFetch = Array(hashes[..<numberOfStories])
|
||||
|
||||
caller.retrieveStories(hashes: hashesToFetch) { result in
|
||||
switch result {
|
||||
case .success((let stories, let date)):
|
||||
self.processStories(account: account, stories: stories) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
if case .failure(let error) = result {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
self.refreshUnreadStories(for: account, hashes: Array(hashes[numberOfStories...]), updateFetchDate: date) { result in
|
||||
os_log(.debug, log: self.log, "Done refreshing stories.")
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mapStoriesToParsedItems(stories: [NewsBlurStory]?) -> Set<ParsedItem> {
|
||||
guard let stories = stories else { return Set<ParsedItem>() }
|
||||
|
||||
let parsedItems: [ParsedItem] = stories.map { story in
|
||||
let author = Set([ParsedAuthor(name: story.authorName, url: nil, avatarURL: nil, emailAddress: nil)])
|
||||
return ParsedItem(syncServiceID: story.storyID, uniqueID: String(story.storyID), feedURL: String(story.feedID), url: story.url, externalURL: nil, title: story.title, contentHTML: story.contentHTML, contentText: nil, summary: nil, imageURL: story.imageURL, bannerImageURL: nil, datePublished: story.datePublished, dateModified: nil, authors: author, tags: Set(story.tags ?? []), attachments: nil)
|
||||
}
|
||||
|
||||
return Set(parsedItems)
|
||||
}
|
||||
|
||||
func sendStoryStatuses(_ statuses: [SyncStatus],
|
||||
throttle: Bool,
|
||||
apiCall: ([String], @escaping (Result<Void, Error>) -> Void) -> Void,
|
||||
completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard !statuses.isEmpty else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
let storyHashes = statuses.compactMap { $0.articleID }
|
||||
let storyHashGroups = storyHashes.chunked(into: throttle ? 1 : 5) // api limit
|
||||
for storyHashGroup in storyHashGroups {
|
||||
group.enter()
|
||||
apiCall(storyHashGroup) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.database.deleteSelectedForProcessing(storyHashGroup.map { String($0) } )
|
||||
group.leave()
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Story status sync call failed: %@.", error.localizedDescription)
|
||||
self.database.resetSelectedForProcessing(storyHashGroup.map { String($0) } )
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncStoryReadState(account: Account, hashes: [NewsBlurStoryHash]?) {
|
||||
guard let hashes = hashes else { return }
|
||||
|
||||
database.selectPendingReadStatusArticleIDs() { result in
|
||||
func process(_ pendingStoryHashes: Set<String>) {
|
||||
|
||||
let newsBlurUnreadStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurUnreadStoryHashes.subtracting(pendingStoryHashes)
|
||||
|
||||
account.fetchUnreadArticleIDs { articleIDsResult in
|
||||
guard let currentUnreadArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
// Mark articles as unread
|
||||
let deltaUnreadArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentUnreadArticleIDs)
|
||||
account.markAsUnread(deltaUnreadArticleIDs)
|
||||
|
||||
// Mark articles as read
|
||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
||||
account.markAsRead(deltaReadArticleIDs)
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Story Read Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncStoryStarredState(account: Account, hashes: [NewsBlurStoryHash]?) {
|
||||
guard let hashes = hashes else { return }
|
||||
|
||||
database.selectPendingStarredStatusArticleIDs() { result in
|
||||
func process(_ pendingStoryHashes: Set<String>) {
|
||||
|
||||
let newsBlurStarredStoryHashes = Set(hashes.map { $0.hash } )
|
||||
let updatableNewsBlurUnreadStoryHashes = newsBlurStarredStoryHashes.subtracting(pendingStoryHashes)
|
||||
|
||||
account.fetchStarredArticleIDs { articleIDsResult in
|
||||
guard let currentStarredArticleIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
// Mark articles as starred
|
||||
let deltaStarredArticleIDs = updatableNewsBlurUnreadStoryHashes.subtracting(currentStarredArticleIDs)
|
||||
account.markAsStarred(deltaStarredArticleIDs)
|
||||
|
||||
// Mark articles as unstarred
|
||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableNewsBlurUnreadStoryHashes)
|
||||
account.markAsUnstarred(deltaUnstarredArticleIDs)
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let pendingArticleIDs):
|
||||
process(pendingArticleIDs)
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Sync Story Starred Status failed: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createFeed(account: Account, feed: NewsBlurFeed?, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
guard let feed = feed else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let webFeed = account.createWebFeed(with: feed.name, url: feed.feedURL, webFeedID: String(feed.feedID), homePageURL: feed.homePageURL)
|
||||
webFeed.externalID = String(feed.feedID)
|
||||
webFeed.faviconURL = feed.faviconURL
|
||||
|
||||
account.addWebFeed(webFeed, to: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
if let name = name {
|
||||
account.renameWebFeed(webFeed, to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.initialFeedDownload(account: account, feed: webFeed, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.initialFeedDownload(account: account, feed: webFeed, completion: completion)
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFeed(account: Account, feed: WebFeed, page: Int, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
caller.retrieveStories(feedID: feed.webFeedID, page: page) { result in
|
||||
switch result {
|
||||
case .success((let stories, _)):
|
||||
// No more stories
|
||||
guard let stories = stories, stories.count > 0 else {
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
let since: Date? = Calendar.current.date(byAdding: .month, value: -3, to: Date())
|
||||
|
||||
self.processStories(account: account, stories: stories, since: since) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
if case .failure(let error) = result {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
// No more recent stories
|
||||
if case .success(let hasStories) = result, !hasStories {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
self.downloadFeed(account: account, feed: feed, page: page + 1, completion: completion)
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initialFeedDownload(account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
// Download the initial articles
|
||||
downloadFeed(account: account, feed: feed, page: 1) { result in
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshMissingStories(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteFeed(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
// This error should never happen
|
||||
guard let feedID = feed.externalID else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
let folderName = (container as? Folder)?.name
|
||||
caller.deleteFeed(feedID: feedID, folder: folderName) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
let feedID = feed.webFeedID
|
||||
|
||||
if folderName == nil {
|
||||
account.removeWebFeed(feed)
|
||||
}
|
||||
|
||||
if let folders = account.folders {
|
||||
for folder in folders where folderName != nil && folder.name == folderName {
|
||||
folder.removeWebFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
if account.existingWebFeed(withWebFeedID: feedID) != nil {
|
||||
account.clearWebFeedMetadata(feed)
|
||||
}
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// NewsBlurFeed.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh Quang Do on 2020-03-09.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSParser
|
||||
|
||||
typealias NewsBlurFolder = NewsBlurFeedsResponse.Folder
|
||||
|
||||
struct NewsBlurFeed: Hashable, Codable {
|
||||
let name: String
|
||||
let feedID: Int
|
||||
let feedURL: String
|
||||
let homePageURL: String?
|
||||
let faviconURL: String?
|
||||
}
|
||||
|
||||
struct NewsBlurFeedsResponse: Decodable {
|
||||
let feeds: [NewsBlurFeed]
|
||||
let folders: [Folder]
|
||||
|
||||
struct Folder: Hashable, Codable {
|
||||
let name: String
|
||||
let feedIDs: [Int]
|
||||
}
|
||||
}
|
||||
|
||||
struct NewsBlurAddURLResponse: Decodable {
|
||||
let feed: NewsBlurFeed?
|
||||
}
|
||||
|
||||
struct NewsBlurFolderRelationship {
|
||||
let folderName: String
|
||||
let feedID: Int
|
||||
}
|
||||
|
||||
extension NewsBlurFeed {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name = "feed_title"
|
||||
case feedID = "id"
|
||||
case feedURL = "feed_address"
|
||||
case homePageURL = "feed_link"
|
||||
case faviconURL = "favicon_url"
|
||||
}
|
||||
}
|
||||
|
||||
extension NewsBlurFeedsResponse {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case feeds = "feeds"
|
||||
case folders = "flat_folders"
|
||||
// TODO: flat_folders_with_inactive
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Tricky part: Some feeds listed in `feeds` don't exist in `folders` for some reason
|
||||
// They don't show up on both mobile/web app, so let's filter them out
|
||||
var visibleFeedIDs: [Int] = []
|
||||
|
||||
// Parse folders
|
||||
var folders: [Folder] = []
|
||||
let folderContainer = try container.nestedContainer(keyedBy: NewsBlurGenericCodingKeys.self, forKey: .folders)
|
||||
|
||||
for key in folderContainer.allKeys {
|
||||
let feedIDs = try folderContainer.decode([Int].self, forKey: key)
|
||||
let folder = Folder(name: key.stringValue, feedIDs: feedIDs)
|
||||
|
||||
folders.append(folder)
|
||||
visibleFeedIDs.append(contentsOf: feedIDs)
|
||||
}
|
||||
|
||||
// Parse feeds
|
||||
var feeds: [NewsBlurFeed] = []
|
||||
let feedContainer = try container.nestedContainer(keyedBy: NewsBlurGenericCodingKeys.self, forKey: .feeds)
|
||||
try feedContainer.allKeys.forEach { key in
|
||||
let feed = try feedContainer.decode(NewsBlurFeed.self, forKey: key)
|
||||
feeds.append(feed)
|
||||
}
|
||||
|
||||
self.feeds = feeds.filter { visibleFeedIDs.contains($0.feedID) }
|
||||
self.folders = folders
|
||||
}
|
||||
}
|
||||
|
||||
extension NewsBlurFeedsResponse.Folder {
|
||||
var asRelationships: [NewsBlurFolderRelationship] {
|
||||
return feedIDs.map { NewsBlurFolderRelationship(folderName: name, feedID: $0) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// NewsBlurFeedChange.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh Quang Do on 2020-03-14.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum NewsBlurFeedChange {
|
||||
case add(String, String?)
|
||||
case rename(String, String)
|
||||
case delete(String, String?)
|
||||
case move(String, String?, String?)
|
||||
}
|
||||
|
||||
extension NewsBlurFeedChange: NewsBlurDataConvertible {
|
||||
var asData: Data? {
|
||||
var postData = URLComponents()
|
||||
postData.queryItems = {
|
||||
switch self {
|
||||
case .add(let url, let folder):
|
||||
return [
|
||||
URLQueryItem(name: "url", value: url),
|
||||
folder != nil ? URLQueryItem(name: "folder", value: folder) : nil
|
||||
].compactMap { $0 }
|
||||
case .rename(let feedID, let newName):
|
||||
return [
|
||||
URLQueryItem(name: "feed_id", value: feedID),
|
||||
URLQueryItem(name: "feed_title", value: newName),
|
||||
]
|
||||
case .delete(let feedID, let folder):
|
||||
return [
|
||||
URLQueryItem(name: "feed_id", value: feedID),
|
||||
folder != nil ? URLQueryItem(name: "in_folder", value: folder) : nil,
|
||||
].compactMap { $0 }
|
||||
case .move(let feedID, let from, let to):
|
||||
return [
|
||||
URLQueryItem(name: "feed_id", value: feedID),
|
||||
URLQueryItem(name: "in_folder", value: from ?? ""),
|
||||
URLQueryItem(name: "to_folder", value: to ?? ""),
|
||||
]
|
||||
}
|
||||
}()
|
||||
|
||||
return postData.percentEncodedQuery?.data(using: .utf8)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// NewsBlurFolderChange.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh Quang Do on 2020-03-14.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum NewsBlurFolderChange {
|
||||
case add(String)
|
||||
case rename(String, String)
|
||||
case delete(String, [String])
|
||||
}
|
||||
|
||||
extension NewsBlurFolderChange: NewsBlurDataConvertible {
|
||||
var asData: Data? {
|
||||
var postData = URLComponents()
|
||||
postData.queryItems = {
|
||||
switch self {
|
||||
case .add(let name):
|
||||
return [
|
||||
URLQueryItem(name: "folder", value: name),
|
||||
URLQueryItem(name: "parent_folder", value: ""), // root folder
|
||||
]
|
||||
case .rename(let from, let to):
|
||||
return [
|
||||
URLQueryItem(name: "folder_to_rename", value: from),
|
||||
URLQueryItem(name: "new_folder_name", value: to),
|
||||
URLQueryItem(name: "in_folder", value: ""), // root folder
|
||||
]
|
||||
case .delete(let name, let feedIDs):
|
||||
var queryItems = [
|
||||
URLQueryItem(name: "folder_to_delete", value: name),
|
||||
URLQueryItem(name: "in_folder", value: ""), // root folder
|
||||
]
|
||||
queryItems.append(contentsOf: feedIDs.map { id in
|
||||
URLQueryItem(name: "feed_id", value: id)
|
||||
})
|
||||
return queryItems
|
||||
}
|
||||
}()
|
||||
|
||||
return postData.percentEncodedQuery?.data(using: .utf8)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// NewsBlurGenericCodingKeys.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh Quang Do on 2020-03-10.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct NewsBlurGenericCodingKeys: CodingKey {
|
||||
var stringValue: String
|
||||
|
||||
init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
}
|
||||
|
||||
var intValue: Int? {
|
||||
return nil
|
||||
}
|
||||
|
||||
init?(intValue: Int) {
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// NewsBlurLoginResponse.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh Quang Do on 2020-03-09.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct NewsBlurLoginResponse: Decodable {
|
||||
var code: Int
|
||||
var errors: LoginError?
|
||||
|
||||
struct LoginError: Decodable {
|
||||
var username: [String]?
|
||||
var others: [String]?
|
||||
}
|
||||
}
|
||||
|
||||
extension NewsBlurLoginResponse.LoginError {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case username = "username"
|
||||
case others = "__all__"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// NewsBlurStory.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh Quang Do on 2020-03-10.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSParser
|
||||
|
||||
typealias NewsBlurStory = NewsBlurStoriesResponse.Story
|
||||
|
||||
struct NewsBlurStoriesResponse: Decodable {
|
||||
let stories: [Story]
|
||||
|
||||
struct Story: Decodable {
|
||||
let storyID: String
|
||||
let feedID: Int
|
||||
let title: String?
|
||||
let url: String?
|
||||
let authorName: String?
|
||||
let contentHTML: String?
|
||||
var imageURL: String? {
|
||||
return imageURLs?.first?.value
|
||||
}
|
||||
var tags: [String]?
|
||||
var datePublished: Date? {
|
||||
let interval = (publishedTimestamp as NSString).doubleValue
|
||||
return Date(timeIntervalSince1970: interval)
|
||||
}
|
||||
|
||||
private var imageURLs: [String: String]?
|
||||
private var publishedTimestamp: String
|
||||
}
|
||||
}
|
||||
|
||||
extension NewsBlurStoriesResponse {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case stories = "stories"
|
||||
}
|
||||
}
|
||||
|
||||
extension NewsBlurStoriesResponse.Story {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case storyID = "story_hash"
|
||||
case feedID = "story_feed_id"
|
||||
case title = "story_title"
|
||||
case url = "story_permalink"
|
||||
case authorName = "story_authors"
|
||||
case contentHTML = "story_content"
|
||||
case imageURLs = "secure_image_urls"
|
||||
case tags = "story_tags"
|
||||
case publishedTimestamp = "story_timestamp"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// NewsBlurStoryHash.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh Quang Do on 2020-03-13.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSParser
|
||||
|
||||
typealias NewsBlurStoryHash = NewsBlurStoryHashesResponse.StoryHash
|
||||
|
||||
struct NewsBlurStoryHashesResponse: Decodable {
|
||||
typealias StoryHashDictionary = [String: [StoryHash]]
|
||||
|
||||
var unread: [StoryHash]?
|
||||
var starred: [StoryHash]?
|
||||
|
||||
struct StoryHash: Hashable, Codable {
|
||||
var hash: String
|
||||
var timestamp: Date
|
||||
}
|
||||
}
|
||||
|
||||
extension NewsBlurStoryHashesResponse {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case unread = "unread_feed_story_hashes"
|
||||
case starred = "starred_story_hashes"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Parse unread
|
||||
if let unreadContainer = try? container.nestedContainer(keyedBy: NewsBlurGenericCodingKeys.self, forKey: .unread) {
|
||||
let storyHashes = try NewsBlurStoryHashesResponse.extractHashes(container: unreadContainer)
|
||||
self.unread = storyHashes.values.flatMap { $0 }
|
||||
}
|
||||
|
||||
// Parse starred
|
||||
if let starredContainer = try? container.nestedUnkeyedContainer(forKey: .starred) {
|
||||
self.starred = try NewsBlurStoryHashesResponse.extractArray(container: starredContainer)
|
||||
}
|
||||
}
|
||||
|
||||
static func extractHashes<Key>(container: KeyedDecodingContainer<Key>) throws -> StoryHashDictionary where Key: CodingKey {
|
||||
var dict: StoryHashDictionary = [:]
|
||||
for key in container.allKeys {
|
||||
dict[key.stringValue] = []
|
||||
var hashArrayContainer = try container.nestedUnkeyedContainer(forKey: key)
|
||||
while !hashArrayContainer.isAtEnd {
|
||||
var hashContainer = try hashArrayContainer.nestedUnkeyedContainer()
|
||||
let hash = try hashContainer.decode(String.self)
|
||||
let timestamp = try hashContainer.decode(Date.self)
|
||||
let storyHash = StoryHash(hash: hash, timestamp: timestamp)
|
||||
|
||||
dict[key.stringValue]?.append(storyHash)
|
||||
}
|
||||
}
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
static func extractArray(container: UnkeyedDecodingContainer) throws -> [StoryHash] {
|
||||
var hashes: [StoryHash] = []
|
||||
var hashArrayContainer = container
|
||||
while !hashArrayContainer.isAtEnd {
|
||||
var hashContainer = try hashArrayContainer.nestedUnkeyedContainer()
|
||||
let hash = try hashContainer.decode(String.self)
|
||||
let timestamp = try (hashContainer.decode(String.self) as NSString).doubleValue
|
||||
let storyHash = StoryHash(hash: hash, timestamp: Date(timeIntervalSince1970: timestamp))
|
||||
|
||||
hashes.append(storyHash)
|
||||
}
|
||||
|
||||
return hashes
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// NewsBlurStoryStatusChange.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh Quang Do on 2020-03-13.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct NewsBlurStoryStatusChange {
|
||||
let hashes: [String]
|
||||
}
|
||||
|
||||
extension NewsBlurStoryStatusChange: NewsBlurDataConvertible {
|
||||
var asData: Data? {
|
||||
var postData = URLComponents()
|
||||
postData.queryItems = hashes.map { URLQueryItem(name: "story_hash", value: $0) }
|
||||
|
||||
return postData.percentEncodedQuery?.data(using: .utf8)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
//
|
||||
// NewsBlurAPICaller.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh-Quang Do on 3/9/20.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSWeb
|
||||
|
||||
final class NewsBlurAPICaller: NSObject {
|
||||
static let SessionIdCookie = "newsblur_sessionid"
|
||||
|
||||
let baseURL = URL(string: "https://www.newsblur.com/")!
|
||||
var transport: Transport!
|
||||
var suspended = false
|
||||
|
||||
var credentials: Credentials?
|
||||
weak var accountMetadata: AccountMetadata?
|
||||
|
||||
init(transport: Transport!) {
|
||||
super.init()
|
||||
self.transport = transport
|
||||
}
|
||||
|
||||
/// Cancels all pending requests rejects any that come in later
|
||||
func suspend() {
|
||||
transport.cancelAll()
|
||||
suspended = true
|
||||
}
|
||||
|
||||
func resume() {
|
||||
suspended = false
|
||||
}
|
||||
|
||||
func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
||||
requestData(endpoint: "api/login", resultType: NewsBlurLoginResponse.self) { result in
|
||||
switch result {
|
||||
case .success(let response, let payload):
|
||||
guard let url = response.url, let headerFields = response.allHeaderFields as? [String: String], payload?.code != -1 else {
|
||||
let error = payload?.errors?.username ?? payload?.errors?.others
|
||||
if let message = error?.first {
|
||||
completion(.failure(NewsBlurError.general(message: message)))
|
||||
} else {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let username = self.credentials?.username else {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
return
|
||||
}
|
||||
|
||||
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url)
|
||||
for cookie in cookies where cookie.name == Self.SessionIdCookie {
|
||||
let credentials = Credentials(type: .newsBlurSessionId, username: username, secret: cookie.value)
|
||||
completion(.success(credentials))
|
||||
return
|
||||
}
|
||||
|
||||
completion(.failure(NewsBlurError.general(message: "Failed to retrieve session")))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logout(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
requestData(endpoint: "api/logout", completion: completion)
|
||||
}
|
||||
|
||||
func retrieveFeeds(completion: @escaping (Result<([NewsBlurFeed]?, [NewsBlurFolder]?), Error>) -> Void) {
|
||||
let url = baseURL
|
||||
.appendingPathComponent("reader/feeds")
|
||||
.appendingQueryItems([
|
||||
URLQueryItem(name: "flat", value: "true"),
|
||||
URLQueryItem(name: "update_counts", value: "false"),
|
||||
])
|
||||
|
||||
requestData(callURL: url, resultType: NewsBlurFeedsResponse.self) { result in
|
||||
switch result {
|
||||
case .success((_, let payload)):
|
||||
completion(.success((payload?.feeds, payload?.folders)))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveStoryHashes(endpoint: String, completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
|
||||
let url = baseURL
|
||||
.appendingPathComponent(endpoint)
|
||||
.appendingQueryItems([
|
||||
URLQueryItem(name: "include_timestamps", value: "true"),
|
||||
])
|
||||
|
||||
requestData(
|
||||
callURL: url,
|
||||
resultType: NewsBlurStoryHashesResponse.self,
|
||||
dateDecoding: .secondsSince1970
|
||||
) { result in
|
||||
switch result {
|
||||
case .success((_, let payload)):
|
||||
let hashes = payload?.unread ?? payload?.starred
|
||||
completion(.success(hashes))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveUnreadStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
|
||||
retrieveStoryHashes(
|
||||
endpoint: "reader/unread_story_hashes",
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
func retrieveStarredStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
|
||||
retrieveStoryHashes(
|
||||
endpoint: "reader/starred_story_hashes",
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
func retrieveStories(feedID: String, page: Int, completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
|
||||
let url = baseURL
|
||||
.appendingPathComponent("reader/feed/\(feedID)")
|
||||
.appendingQueryItems([
|
||||
URLQueryItem(name: "page", value: String(page)),
|
||||
URLQueryItem(name: "order", value: "newest"),
|
||||
URLQueryItem(name: "read_filter", value: "all"),
|
||||
URLQueryItem(name: "include_hidden", value: "true"),
|
||||
URLQueryItem(name: "include_story_content", value: "true"),
|
||||
])
|
||||
|
||||
requestData(callURL: url, resultType: NewsBlurStoriesResponse.self) { result in
|
||||
switch result {
|
||||
case .success(let (response, payload)):
|
||||
completion(.success((payload?.stories, HTTPDateInfo(urlResponse: response)?.date)))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
|
||||
let url = baseURL
|
||||
.appendingPathComponent("reader/river_stories")
|
||||
.appendingQueryItem(.init(name: "include_hidden", value: "true"))?
|
||||
.appendingQueryItems(hashes.map {
|
||||
URLQueryItem(name: "h", value: $0.hash)
|
||||
})
|
||||
|
||||
requestData(callURL: url, resultType: NewsBlurStoriesResponse.self) { result in
|
||||
switch result {
|
||||
case .success(let (response, payload)):
|
||||
completion(.success((payload?.stories, HTTPDateInfo(urlResponse: response)?.date)))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markAsUnread(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
sendUpdates(
|
||||
endpoint: "reader/mark_story_hash_as_unread",
|
||||
payload: NewsBlurStoryStatusChange(hashes: hashes),
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
func markAsRead(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
sendUpdates(
|
||||
endpoint: "reader/mark_story_hashes_as_read",
|
||||
payload: NewsBlurStoryStatusChange(hashes: hashes),
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
func star(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
sendUpdates(
|
||||
endpoint: "reader/mark_story_hash_as_starred",
|
||||
payload: NewsBlurStoryStatusChange(hashes: hashes),
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
func unstar(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
sendUpdates(
|
||||
endpoint: "reader/mark_story_hash_as_unstarred",
|
||||
payload: NewsBlurStoryStatusChange(hashes: hashes),
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
func addFolder(named name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
sendUpdates(
|
||||
endpoint: "reader/add_folder",
|
||||
payload: NewsBlurFolderChange.add(name),
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
func renameFolder(with folder: String, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
sendUpdates(
|
||||
endpoint: "reader/rename_folder",
|
||||
payload: NewsBlurFolderChange.rename(folder, name),
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
func removeFolder(named name: String, feedIDs: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
sendUpdates(
|
||||
endpoint: "reader/delete_folder",
|
||||
payload: NewsBlurFolderChange.delete(name, feedIDs),
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
func addURL(_ url: String, folder: String?, completion: @escaping (Result<NewsBlurFeed?, Error>) -> Void) {
|
||||
sendUpdates(
|
||||
endpoint: "reader/add_url",
|
||||
payload: NewsBlurFeedChange.add(url, folder),
|
||||
resultType: NewsBlurAddURLResponse.self
|
||||
) { result in
|
||||
switch result {
|
||||
case .success(_, let payload):
|
||||
completion(.success(payload?.feed))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renameFeed(feedID: String, newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
sendUpdates(
|
||||
endpoint: "reader/rename_feed",
|
||||
payload: NewsBlurFeedChange.rename(feedID, newName)
|
||||
) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteFeed(feedID: String, folder: String? = nil, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
sendUpdates(
|
||||
endpoint: "reader/delete_feed",
|
||||
payload: NewsBlurFeedChange.delete(feedID, folder)
|
||||
) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func moveFeed(feedID: String, from: String?, to: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
sendUpdates(
|
||||
endpoint: "reader/move_feed_to_folder",
|
||||
payload: NewsBlurFeedChange.move(feedID, from, to)
|
||||
) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,621 @@
|
|||
//
|
||||
// NewsBlurAccountDelegate.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Anh-Quang Do on 3/9/20.
|
||||
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Articles
|
||||
import RSCore
|
||||
import RSDatabase
|
||||
import RSParser
|
||||
import RSWeb
|
||||
import SyncDatabase
|
||||
import os.log
|
||||
|
||||
final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
|
||||
var behaviors: AccountBehaviors = []
|
||||
|
||||
var isOPMLImportInProgress: Bool = false
|
||||
var server: String? = "newsblur.com"
|
||||
var credentials: Credentials? {
|
||||
didSet {
|
||||
caller.credentials = credentials
|
||||
}
|
||||
}
|
||||
|
||||
var accountMetadata: AccountMetadata? = nil
|
||||
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||
|
||||
let caller: NewsBlurAPICaller
|
||||
let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "NewsBlur")
|
||||
let database: SyncDatabase
|
||||
|
||||
init(dataFolder: String, transport: Transport?) {
|
||||
if let transport = transport {
|
||||
caller = NewsBlurAPICaller(transport: transport)
|
||||
} else {
|
||||
let sessionConfiguration = URLSessionConfiguration.default
|
||||
sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
||||
sessionConfiguration.timeoutIntervalForRequest = 60.0
|
||||
sessionConfiguration.httpShouldSetCookies = false
|
||||
sessionConfiguration.httpCookieAcceptPolicy = .never
|
||||
sessionConfiguration.httpMaximumConnectionsPerHost = 1
|
||||
sessionConfiguration.httpCookieStorage = nil
|
||||
sessionConfiguration.urlCache = nil
|
||||
|
||||
if let userAgentHeaders = UserAgent.headers() {
|
||||
sessionConfiguration.httpAdditionalHeaders = userAgentHeaders
|
||||
}
|
||||
|
||||
let session = URLSession(configuration: sessionConfiguration)
|
||||
caller = NewsBlurAPICaller(transport: session)
|
||||
}
|
||||
|
||||
database = SyncDatabase(databaseFilePath: dataFolder.appending("/DB.sqlite3"))
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(5)
|
||||
|
||||
refreshFeeds(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.sendArticleStatus(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshStories(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshMissingStories(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
self.refreshProgress.clear()
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
os_log(.debug, log: log, "Sending story statuses...")
|
||||
|
||||
database.selectForProcessing { result in
|
||||
|
||||
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||
let createUnreadStatuses = syncStatuses.filter {
|
||||
$0.key == ArticleStatus.Key.read && $0.flag == false
|
||||
}
|
||||
let deleteUnreadStatuses = syncStatuses.filter {
|
||||
$0.key == ArticleStatus.Key.read && $0.flag == true
|
||||
}
|
||||
let createStarredStatuses = syncStatuses.filter {
|
||||
$0.key == ArticleStatus.Key.starred && $0.flag == true
|
||||
}
|
||||
let deleteStarredStatuses = syncStatuses.filter {
|
||||
$0.key == ArticleStatus.Key.starred && $0.flag == false
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(createUnreadStatuses, throttle: true, apiCall: self.caller.markAsUnread) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(deleteUnreadStatuses, throttle: false, apiCall: self.caller.markAsRead) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(createStarredStatuses, throttle: true, apiCall: self.caller.star) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.sendStoryStatuses(deleteStarredStatuses, throttle: true, apiCall: self.caller.unstar) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let syncStatuses):
|
||||
processStatuses(syncStatuses)
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
os_log(.debug, log: log, "Refreshing story statuses...")
|
||||
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
group.enter()
|
||||
caller.retrieveUnreadStoryHashes { result in
|
||||
switch result {
|
||||
case .success(let storyHashes):
|
||||
self.syncStoryReadState(account: account, hashes: storyHashes)
|
||||
group.leave()
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.info, log: self.log, "Retrieving unread stories failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.enter()
|
||||
caller.retrieveStarredStoryHashes { result in
|
||||
switch result {
|
||||
case .success(let storyHashes):
|
||||
self.syncStoryStarredState(account: account, hashes: storyHashes)
|
||||
group.leave()
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.info, log: self.log, "Retrieving starred stories failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done refreshing article statuses.")
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshStories(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
os_log(.debug, log: log, "Refreshing stories...")
|
||||
os_log(.debug, log: log, "Refreshing unread stories...")
|
||||
|
||||
caller.retrieveUnreadStoryHashes { result in
|
||||
switch result {
|
||||
case .success(let storyHashes):
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
if let count = storyHashes?.count, count > 0 {
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining((count - 1) / 100 + 1)
|
||||
}
|
||||
|
||||
self.refreshUnreadStories(for: account, hashes: storyHashes, updateFetchDate: nil, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshMissingStories(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
os_log(.debug, log: log, "Refreshing missing stories...")
|
||||
|
||||
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
|
||||
|
||||
func process(_ fetchedHashes: Set<String>) {
|
||||
let group = DispatchGroup()
|
||||
var errorOccurred = false
|
||||
|
||||
let storyHashes = Array(fetchedHashes).map {
|
||||
NewsBlurStoryHash(hash: $0, timestamp: Date())
|
||||
}
|
||||
let chunkedStoryHashes = storyHashes.chunked(into: 100)
|
||||
|
||||
for chunk in chunkedStoryHashes {
|
||||
group.enter()
|
||||
self.caller.retrieveStories(hashes: chunk) { result in
|
||||
|
||||
switch result {
|
||||
case .success((let stories, _)):
|
||||
self.processStories(account: account, stories: stories) { result in
|
||||
group.leave()
|
||||
if case .failure = result {
|
||||
errorOccurred = true
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
errorOccurred = true
|
||||
os_log(.error, log: self.log, "Refresh missing stories failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
self.refreshProgress.completeTask()
|
||||
os_log(.debug, log: self.log, "Done refreshing missing stories.")
|
||||
if errorOccurred {
|
||||
completion(.failure(NewsBlurError.unknown))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let fetchedArticleIDs):
|
||||
process(fetchedArticleIDs)
|
||||
case .failure(let error):
|
||||
self.refreshProgress.completeTask()
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processStories(account: Account, stories: [NewsBlurStory]?, since: Date? = nil, completion: @escaping (Result<Bool, DatabaseError>) -> Void) {
|
||||
let parsedItems = mapStoriesToParsedItems(stories: stories).filter {
|
||||
guard let datePublished = $0.datePublished, let since = since else {
|
||||
return true
|
||||
}
|
||||
|
||||
return datePublished >= since
|
||||
}
|
||||
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL }).mapValues {
|
||||
Set($0)
|
||||
}
|
||||
|
||||
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true) { error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
completion(.success(!webFeedIDsAndItems.isEmpty))
|
||||
}
|
||||
}
|
||||
|
||||
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> ()) {
|
||||
self.refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
caller.addFolder(named: name) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success():
|
||||
if let folder = account.ensureFolder(with: name) {
|
||||
completion(.success(folder))
|
||||
} else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let folderToRename = folder.name else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
let nameBefore = folder.name
|
||||
|
||||
caller.renameFolder(with: folderToRename, to: name) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
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, Error>) -> ()) {
|
||||
guard let folderToRemove = folder.name else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
var feedIDs: [String] = []
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
if (feed.folderRelationship?.count ?? 0) > 1 {
|
||||
clearFolderRelationship(for: feed, withFolderName: folderToRemove)
|
||||
} else if let feedID = feed.externalID {
|
||||
feedIDs.append(feedID)
|
||||
}
|
||||
}
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
caller.removeFolder(named: folderToRemove, feedIDs: feedIDs) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
account.removeFolder(folder)
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> ()) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
let folderName = (container as? Folder)?.name
|
||||
caller.addURL(url, folder: folderName) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
self.createFeed(account: account, feed: feed, name: name, container: container, completion: completion)
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let feedID = feed.externalID else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
caller.renameFeed(feedID: feedID, newName: name) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
feed.editedName = name
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let folder = container as? Folder else {
|
||||
DispatchQueue.main.async {
|
||||
if let account = container as? Account {
|
||||
account.addWebFeed(feed)
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let folderName = folder.name ?? ""
|
||||
saveFolderRelationship(for: feed, withFolderName: folderName, id: folderName)
|
||||
folder.addWebFeed(feed)
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
deleteFeed(for: account, with: feed, from: container, completion: completion)
|
||||
}
|
||||
|
||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let feedID = feed.externalID else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||
|
||||
caller.moveFeed(
|
||||
feedID: feedID,
|
||||
from: (from as? Folder)?.name,
|
||||
to: (to as? Folder)?.name
|
||||
) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
from.removeWebFeed(feed)
|
||||
to.addWebFeed(feed)
|
||||
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
||||
account.addWebFeed(existingFeed, to: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
createWebFeed(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, Error>) -> ()) {
|
||||
guard let folderName = folder.name else {
|
||||
completion(.failure(NewsBlurError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
var feedsToRestore: [WebFeed] = []
|
||||
for feed in folder.topLevelWebFeeds {
|
||||
feedsToRestore.append(feed)
|
||||
folder.topLevelWebFeeds.remove(feed)
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
group.enter()
|
||||
addFolder(for: account, name: folderName) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success(let folder):
|
||||
for feed in feedsToRestore {
|
||||
group.enter()
|
||||
self.restoreWebFeed(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Restore folder feed error: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
||||
let syncStatuses = articles.map { article in
|
||||
return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag)
|
||||
}
|
||||
database.insertStatuses(syncStatuses)
|
||||
|
||||
database.selectPendingCount { result in
|
||||
if let count = try? result.get(), count > 100 {
|
||||
self.sendArticleStatus(for: account) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
return try? account.update(articles, statusKey: statusKey, flag: flag)
|
||||
}
|
||||
|
||||
func accountDidInitialize(_ account: Account) {
|
||||
credentials = try? account.retrieveCredentials(type: .newsBlurSessionId)
|
||||
}
|
||||
|
||||
func accountWillBeDeleted(_ account: Account) {
|
||||
caller.logout() { _ in }
|
||||
}
|
||||
|
||||
class func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> ()) {
|
||||
let caller = NewsBlurAPICaller(transport: transport)
|
||||
caller.credentials = credentials
|
||||
caller.validateCredentials() { result in
|
||||
DispatchQueue.main.async {
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Suspend and Resume (for iOS)
|
||||
|
||||
/// Suspend all network activity
|
||||
func suspendNetwork() {
|
||||
caller.suspend()
|
||||
}
|
||||
|
||||
/// Suspend the SQLLite databases
|
||||
func suspendDatabase() {
|
||||
database.suspend()
|
||||
}
|
||||
|
||||
/// Make sure no SQLite databases are open and we are ready to issue network requests.
|
||||
func resume() {
|
||||
caller.resume()
|
||||
database.resume()
|
||||
}
|
||||
}
|
|
@ -292,7 +292,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// This error should never happen
|
||||
guard let subscriptionID = feed.subscriptionID else {
|
||||
guard let subscriptionID = feed.externalID else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
@ -340,12 +340,12 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||
|
||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let folder = container as? Folder, let feedName = feed.subscriptionID {
|
||||
if let folder = container as? Folder, let feedName = feed.externalID {
|
||||
caller.createTagging(subscriptionID: feedName, tagName: folder.name ?? "") { result in
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: feed.subscriptionID!)
|
||||
self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: feed.externalID!)
|
||||
account.removeWebFeed(feed)
|
||||
folder.addWebFeed(feed)
|
||||
completion(.success(()))
|
||||
|
@ -582,7 +582,7 @@ private extension ReaderAPIAccountDelegate {
|
|||
} else {
|
||||
let feed = account.createWebFeed(with: subscription.name, url: subscription.url, webFeedID: subFeedId, homePageURL: subscription.homePageURL)
|
||||
feed.iconURL = subscription.iconURL
|
||||
feed.subscriptionID = String(subscription.feedID)
|
||||
feed.externalID = String(subscription.feedID)
|
||||
account.addWebFeed(feed)
|
||||
}
|
||||
|
||||
|
@ -758,7 +758,7 @@ private extension ReaderAPIAccountDelegate {
|
|||
DispatchQueue.main.async {
|
||||
|
||||
let feed = account.createWebFeed(with: sub.name, url: sub.url, webFeedID: String(sub.feedID), homePageURL: sub.homePageURL)
|
||||
feed.subscriptionID = String(sub.feedID)
|
||||
feed.externalID = String(sub.feedID)
|
||||
|
||||
account.addWebFeed(feed, to: container) { result in
|
||||
switch result {
|
||||
|
@ -985,7 +985,7 @@ private extension ReaderAPIAccountDelegate {
|
|||
|
||||
func deleteTagging(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let folder = container as? Folder, let feedName = feed.subscriptionID {
|
||||
if let folder = container as? Folder, let feedName = feed.externalID {
|
||||
caller.deleteTagging(subscriptionID: feedName, tagName: folder.name ?? "") { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
@ -1014,7 +1014,7 @@ private extension ReaderAPIAccountDelegate {
|
|||
func deleteSubscription(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// This error should never happen
|
||||
guard let subscriptionID = feed.subscriptionID else {
|
||||
guard let subscriptionID = feed.externalID else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -153,12 +153,12 @@ public final class WebFeed: Feed, Renamable, Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
public var subscriptionID: String? {
|
||||
public var externalID: String? {
|
||||
get {
|
||||
return metadata.subscriptionID
|
||||
return metadata.externalID
|
||||
}
|
||||
set {
|
||||
metadata.subscriptionID = newValue
|
||||
metadata.externalID = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ final class WebFeedMetadata: Codable {
|
|||
case isNotifyAboutNewArticles
|
||||
case isArticleExtractorAlwaysOn
|
||||
case conditionalGetInfo
|
||||
case subscriptionID
|
||||
case externalID = "subscriptionID"
|
||||
case folderRelationship
|
||||
}
|
||||
|
||||
|
@ -111,10 +111,10 @@ final class WebFeedMetadata: Codable {
|
|||
}
|
||||
}
|
||||
|
||||
var subscriptionID: String? {
|
||||
var externalID: String? {
|
||||
didSet {
|
||||
if subscriptionID != oldValue {
|
||||
valueDidChange(.subscriptionID)
|
||||
if externalID != oldValue {
|
||||
valueDidChange(.externalID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15505"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15705"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
|
@ -343,18 +343,6 @@
|
|||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||
<items>
|
||||
<menuItem title="Clean Up" keyEquivalent="H" id="J5h-uQ-57w">
|
||||
<connections>
|
||||
<action selector="cleanUp:" target="Ady-hI-5gd" id="eNB-UA-e3a"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="rcJ-r3-Y2U"/>
|
||||
<menuItem title="Hide Read Articles" id="b10-sA-Yzi">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleReadArticlesFilter:" target="Ady-hI-5gd" id="YhV-0F-jrM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Sort Articles By" id="nLP-fa-KUi">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Sort Articles By" id="OlJ-93-6OP">
|
||||
|
@ -381,12 +369,22 @@
|
|||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="dZt-2W-gxf"/>
|
||||
<menuItem title="Hide Read Feeds" id="E9K-zV-nLv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menuItem title="Clean Up Articles" keyEquivalent="'" id="J5h-uQ-57w">
|
||||
<connections>
|
||||
<action selector="cleanUp:" target="Ady-hI-5gd" id="eNB-UA-e3a"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Read Articles" keyEquivalent="H" id="b10-sA-Yzi">
|
||||
<connections>
|
||||
<action selector="toggleReadArticlesFilter:" target="Ady-hI-5gd" id="YhV-0F-jrM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Read Feeds" keyEquivalent="F" id="E9K-zV-nLv">
|
||||
<connections>
|
||||
<action selector="toggleReadFeedsFilter:" target="Ady-hI-5gd" id="5pI-YT-xai"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="Ocl-6H-m3k"/>
|
||||
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
|
|
|
@ -154,7 +154,10 @@ code, pre {
|
|||
border: 1px solid var(--accent-color);
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.nnw-overflow table table {
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
.nnw-overflow td, .nnw-overflow th {
|
||||
-webkit-hyphens: none;
|
||||
word-break: normal;
|
||||
|
|
|
@ -218,6 +218,7 @@ protocol SidebarDelegate: class {
|
|||
}
|
||||
|
||||
@objc func downloadArticlesDidUpdateUnreadCounts(_ note: Notification) {
|
||||
addTreeControllerToFilterExceptions()
|
||||
rebuildTreeAndRestoreSelection()
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +2,18 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.$(ORGANIZATION_IDENTIFIER).NetNewsWire</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
|
|
|
@ -260,6 +260,8 @@
|
|||
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB3C229AB08300645299 /* ErrorHandler.swift */; };
|
||||
51E43962238037C400015C31 /* AddWebFeedFolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E43961238037C400015C31 /* AddWebFeedFolderViewController.swift */; };
|
||||
51E4398023805EBC00015C31 /* AddWebFeedFolderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4397F23805EBC00015C31 /* AddWebFeedFolderTableViewCell.swift */; };
|
||||
51E4DAED2425F6940091EB5B /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4DAEC2425F6940091EB5B /* CloudKit.framework */; };
|
||||
51E4DB082425F9EB0091EB5B /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4DB072425F9EB0091EB5B /* CloudKit.framework */; };
|
||||
51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; };
|
||||
51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; };
|
||||
51EAED96231363EF00A9EEE3 /* NonIntrinsicButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EAED95231363EF00A9EEE3 /* NonIntrinsicButton.swift */; };
|
||||
|
@ -464,7 +466,6 @@
|
|||
65ED405D235DEF6C0081F399 /* SidebarKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */; };
|
||||
65ED405E235DEF6C0081F399 /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; };
|
||||
65ED405F235DEF6C0081F399 /* Preferences.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FC8022629E4800D921D6 /* Preferences.storyboard */; };
|
||||
65ED4060235DEF6C0081F399 /* (null) in Resources */ = {isa = PBXBuildFile; };
|
||||
65ED4061235DEF6C0081F399 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 849C64671ED37A5D003D8FC0 /* Assets.xcassets */; };
|
||||
65ED4062235DEF6C0081F399 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 848362FC2262A30800DA1D35 /* styleSheet.css */; };
|
||||
65ED4063235DEF6C0081F399 /* RenameSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 848363092262A3F000DA1D35 /* RenameSheet.xib */; };
|
||||
|
@ -503,6 +504,7 @@
|
|||
65ED42DD235E74230081F399 /* org.sparkle-project.InstallerStatus.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 65ED42BA235E71B40081F399 /* org.sparkle-project.InstallerStatus.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
65ED42DE235E74230081F399 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65ED42B0235E71B40081F399 /* Sparkle.framework */; };
|
||||
65ED42DF235E74230081F399 /* Sparkle.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 65ED42B0235E71B40081F399 /* Sparkle.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
769F2ED513DA03EE75B993A8 /* NewsBlurAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 769F2D3643779DB02786278E /* NewsBlurAccountViewController.swift */; };
|
||||
8405DD8A2213E0E3008CE1BF /* DetailContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405DD892213E0E3008CE1BF /* DetailContainerView.swift */; };
|
||||
8405DD9922153B6B008CE1BF /* TimelineContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405DD9822153B6B008CE1BF /* TimelineContainerView.swift */; };
|
||||
8405DD9C22153BD7008CE1BF /* NSView-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405DD9B22153BD7008CE1BF /* NSView-Extensions.swift */; };
|
||||
|
@ -1402,6 +1404,8 @@
|
|||
51E3EB3C229AB08300645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
|
||||
51E43961238037C400015C31 /* AddWebFeedFolderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedFolderViewController.swift; sourceTree = "<group>"; };
|
||||
51E4397F23805EBC00015C31 /* AddWebFeedFolderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedFolderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
51E4DAEC2425F6940091EB5B /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
|
||||
51E4DB072425F9EB0091EB5B /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk/System/Library/Frameworks/CloudKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleStatusSyncTimer.swift; sourceTree = "<group>"; };
|
||||
51EAED95231363EF00A9EEE3 /* NonIntrinsicButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicButton.swift; sourceTree = "<group>"; };
|
||||
51EC114B2149FE3300B296E3 /* FolderTreeMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FolderTreeMenu.swift; path = AddFeed/FolderTreeMenu.swift; sourceTree = "<group>"; };
|
||||
|
@ -1450,6 +1454,7 @@
|
|||
65ED40F2235DF5E00081F399 /* NetNewsWire_macapp_target_macappstore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_macapp_target_macappstore.xcconfig; sourceTree = "<group>"; };
|
||||
65ED4186235E045B0081F399 /* NetNewsWire_safariextension_target_macappstore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_safariextension_target_macappstore.xcconfig; sourceTree = "<group>"; };
|
||||
65ED4299235E71B40081F399 /* Sparkle.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Sparkle.xcodeproj; path = submodules/Sparkle/Sparkle.xcodeproj; sourceTree = SOURCE_ROOT; };
|
||||
769F2D3643779DB02786278E /* NewsBlurAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurAccountViewController.swift; sourceTree = "<group>"; };
|
||||
8405DD892213E0E3008CE1BF /* DetailContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailContainerView.swift; sourceTree = "<group>"; };
|
||||
8405DD9822153B6B008CE1BF /* TimelineContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContainerView.swift; sourceTree = "<group>"; };
|
||||
8405DD9B22153BD7008CE1BF /* NSView-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSView-Extensions.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1725,6 +1730,7 @@
|
|||
51C451F02264C83100C03939 /* ArticlesDatabase.framework in Frameworks */,
|
||||
51C451F42264C83900C03939 /* Articles.framework in Frameworks */,
|
||||
51C451E82264C81000C03939 /* RSDatabase.framework in Frameworks */,
|
||||
51E4DB082425F9EB0091EB5B /* CloudKit.framework in Frameworks */,
|
||||
51C451EC2264C81B00C03939 /* RSCore.framework in Frameworks */,
|
||||
51554C30228B71A10055115A /* SyncDatabase.framework in Frameworks */,
|
||||
51C451E42264C80600C03939 /* RSParser.framework in Frameworks */,
|
||||
|
@ -1744,6 +1750,7 @@
|
|||
84C37FB520DD8DBB00CA8CF5 /* RSParser.framework in Frameworks */,
|
||||
51C451BD226377D000C03939 /* Account.framework in Frameworks */,
|
||||
51C451B9226377C900C03939 /* Articles.framework in Frameworks */,
|
||||
51E4DAED2425F6940091EB5B /* CloudKit.framework in Frameworks */,
|
||||
84C37FA520DD8D8400CA8CF5 /* RSCore.framework in Frameworks */,
|
||||
51554C24228B71910055115A /* SyncDatabase.framework in Frameworks */,
|
||||
);
|
||||
|
@ -1868,6 +1875,7 @@
|
|||
51A1698F235E10D600EB091F /* LocalAccountViewController.swift */,
|
||||
51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */,
|
||||
3B3A328B238B820900314204 /* FeedWranglerAccountViewController.swift */,
|
||||
769F2D3643779DB02786278E /* NewsBlurAccountViewController.swift */,
|
||||
);
|
||||
path = Account;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2078,6 +2086,8 @@
|
|||
51C452B22265141B00C03939 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
51E4DB072425F9EB0091EB5B /* CloudKit.framework */,
|
||||
51E4DAEC2425F6940091EB5B /* CloudKit.framework */,
|
||||
51C452B32265141B00C03939 /* WebKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
|
@ -3479,7 +3489,6 @@
|
|||
5103A9F5242258C600410853 /* AccountsAddCloudKit.xib in Resources */,
|
||||
65ED405E235DEF6C0081F399 /* DefaultFeeds.opml in Resources */,
|
||||
65ED405F235DEF6C0081F399 /* Preferences.storyboard in Resources */,
|
||||
65ED4060235DEF6C0081F399 /* (null) in Resources */,
|
||||
65ED4061235DEF6C0081F399 /* Assets.xcassets in Resources */,
|
||||
65ED4062235DEF6C0081F399 /* styleSheet.css in Resources */,
|
||||
65ED4063235DEF6C0081F399 /* RenameSheet.xib in Resources */,
|
||||
|
@ -4061,6 +4070,7 @@
|
|||
51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */,
|
||||
51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */,
|
||||
51934CCB230F599B006127BE /* InteractiveNavigationController.swift in Sources */,
|
||||
769F2ED513DA03EE75B993A8 /* NewsBlurAccountViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -31,7 +31,7 @@ rm -f buildscripts/certs/ios-dist.cer
|
|||
rm -f buildscripts/certs/mac-dist.p12
|
||||
|
||||
# Do the build
|
||||
xcodebuild -scheme $SCHEME test -destination "$DESTINATION" -showBuildTimingSummary
|
||||
xcodebuild -scheme $SCHEME test -destination "$DESTINATION" -showBuildTimingSummary -allowProvisioningUpdates
|
||||
|
||||
# Delete the keychain and the provisioning profile
|
||||
security delete-keychain github-build.keychain
|
||||
|
|
|
@ -1,167 +1,173 @@
|
|||
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
|
||||
|
|
|
@ -406,6 +406,155 @@
|
|||
</objects>
|
||||
<point key="canvasLocation" x="3177" y="145"/>
|
||||
</scene>
|
||||
<!--Modal Navigation Controller-->
|
||||
<scene sceneID="j4N-ax-exh">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="NewsBlurAccountNavigationViewController" id="eE3-pu-HdL" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="Fsp-NG-hoR">
|
||||
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="Cge-ND-NpD" kind="relationship" relationship="rootViewController" id="1D5-CN-liN"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="8t3-0U-5vL" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4562" y="-528"/>
|
||||
</scene>
|
||||
<!--NewsBlur-->
|
||||
<scene sceneID="tfA-kz-P6O">
|
||||
<objects>
|
||||
<tableViewController id="Cge-ND-NpD" customClass="NewsBlurAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fLL-7i-HdK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<sections>
|
||||
<tableViewSection id="I5T-12-2jC">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="gAY-Bo-c0L">
|
||||
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="gAY-Bo-c0L" id="mqD-6S-DIl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Username or Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="S4v-fs-DIO">
|
||||
<rect key="frame" x="20" y="11.5" width="334" height="21"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="S4v-fs-DIO" secondAttribute="trailing" constant="20" id="Upe-dm-4DP"/>
|
||||
<constraint firstItem="S4v-fs-DIO" firstAttribute="leading" secondItem="mqD-6S-DIl" secondAttribute="leading" constant="20" id="pQc-Fh-6T3"/>
|
||||
<constraint firstItem="S4v-fs-DIO" firstAttribute="centerY" secondItem="mqD-6S-DIl" secondAttribute="centerY" id="s9a-ew-C5W"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="iCK-kn-Au6">
|
||||
<rect key="frame" x="20" y="61.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="iCK-kn-Au6" id="9Ej-wB-9Tr">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="fct-XR-fEa">
|
||||
<rect key="frame" x="20" y="11.5" width="283" height="21"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
|
||||
</textField>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GY9-nr-jFb">
|
||||
<rect key="frame" x="311" y="5.5" width="43" height="33"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Show"/>
|
||||
<connections>
|
||||
<action selector="showHidePassword:" destination="Cge-ND-NpD" eventType="touchUpInside" id="8JH-LX-URH"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="GY9-nr-jFb" firstAttribute="centerY" secondItem="9Ej-wB-9Tr" secondAttribute="centerY" id="3jf-KC-nd8"/>
|
||||
<constraint firstItem="GY9-nr-jFb" firstAttribute="leading" secondItem="fct-XR-fEa" secondAttribute="trailing" constant="8" symbolic="YES" id="Ibr-pt-eGr"/>
|
||||
<constraint firstAttribute="trailing" secondItem="GY9-nr-jFb" secondAttribute="trailing" constant="20" symbolic="YES" id="mcZ-cl-knP"/>
|
||||
<constraint firstItem="fct-XR-fEa" firstAttribute="leading" secondItem="9Ej-wB-9Tr" secondAttribute="leading" constant="20" id="u5f-tJ-8ce"/>
|
||||
<constraint firstItem="fct-XR-fEa" firstAttribute="centerY" secondItem="9Ej-wB-9Tr" secondAttribute="centerY" id="z5e-jg-0nm"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="L37-iZ-GVj">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="fyQ-K8-byV">
|
||||
<rect key="frame" x="20" y="141" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fyQ-K8-byV" id="CtR-ZJ-FG5">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="E1I-C4-JdL" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="yoo-36-msf"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Action">
|
||||
<color key="titleColor" name="secondaryAccentColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="action:" destination="Cge-ND-NpD" eventType="touchUpInside" id="YQw-1k-e8G"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="E1I-C4-JdL" firstAttribute="centerY" secondItem="CtR-ZJ-FG5" secondAttribute="centerY" id="2vc-Ys-4Cj"/>
|
||||
<constraint firstAttribute="trailing" secondItem="E1I-C4-JdL" secondAttribute="trailing" id="SLX-wc-QR7"/>
|
||||
<constraint firstItem="E1I-C4-JdL" firstAttribute="leading" secondItem="CtR-ZJ-FG5" secondAttribute="leading" id="Veu-Wo-GSZ"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="Cge-ND-NpD" id="u8B-p4-Vlv"/>
|
||||
<outlet property="delegate" destination="Cge-ND-NpD" id="RIw-V2-EJC"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="NewsBlur" id="jCQ-pH-6AD">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="bl6-Y1-wQ8">
|
||||
<connections>
|
||||
<action selector="cancel:" destination="Cge-ND-NpD" id="9zR-LJ-IWk"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" id="4yi-H0-B9J">
|
||||
<view key="customView" contentMode="scaleToFill" id="8DU-L0-P6c">
|
||||
<rect key="frame" x="374" y="12" width="20" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="HfW-jV-MjK">
|
||||
<rect key="frame" x="36" y="6" width="20" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</activityIndicatorView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="actionButton" destination="E1I-C4-JdL" id="q2T-4o-c8i"/>
|
||||
<outlet property="activityIndicator" destination="HfW-jV-MjK" id="AIV-uG-9uC"/>
|
||||
<outlet property="cancelBarButtonItem" destination="bl6-Y1-wQ8" id="ohR-gW-5J2"/>
|
||||
<outlet property="passwordTextField" destination="fct-XR-fEa" id="fGL-4k-gZ6"/>
|
||||
<outlet property="showHideButton" destination="GY9-nr-jFb" id="1p9-9F-GMY"/>
|
||||
<outlet property="usernameTextField" destination="S4v-fs-DIO" id="B7I-yz-M0T"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="8Ku-6P-yPg" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4562" y="145"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<namedColor name="secondaryAccentColor">
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
//
|
||||
// NewsBlurAccountViewController.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Anh-Quang Do on 3/9/20.
|
||||
// Copyright (c) 2020 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Account
|
||||
import RSWeb
|
||||
|
||||
class NewsBlurAccountViewController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
|
||||
@IBOutlet weak var cancelBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet weak var usernameTextField: UITextField!
|
||||
@IBOutlet weak var passwordTextField: UITextField!
|
||||
@IBOutlet weak var showHideButton: UIButton!
|
||||
@IBOutlet weak var actionButton: UIButton!
|
||||
|
||||
weak var account: Account?
|
||||
weak var delegate: AddAccountDismissDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
activityIndicator.isHidden = true
|
||||
usernameTextField.delegate = self
|
||||
passwordTextField.delegate = self
|
||||
|
||||
if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) {
|
||||
actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal)
|
||||
actionButton.isEnabled = true
|
||||
usernameTextField.text = credentials.username
|
||||
passwordTextField.text = credentials.secret
|
||||
} else {
|
||||
actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal)
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: usernameTextField)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField)
|
||||
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
if section == 0 {
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
|
||||
headerView.imageView.image = AppAssets.image(for: .newsBlur)
|
||||
return headerView
|
||||
} else {
|
||||
return super.tableView(tableView, viewForHeaderInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
delegate?.dismiss()
|
||||
}
|
||||
|
||||
@IBAction func showHidePassword(_ sender: Any) {
|
||||
if passwordTextField.isSecureTextEntry {
|
||||
passwordTextField.isSecureTextEntry = false
|
||||
showHideButton.setTitle("Hide", for: .normal)
|
||||
} else {
|
||||
passwordTextField.isSecureTextEntry = true
|
||||
showHideButton.setTitle("Show", for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func action(_ sender: Any) {
|
||||
|
||||
guard let username = usernameTextField.text else {
|
||||
showError(NSLocalizedString("Username required.", comment: "Credentials Error"))
|
||||
return
|
||||
}
|
||||
|
||||
let password = passwordTextField.text ?? ""
|
||||
|
||||
startAnimatingActivityIndicator()
|
||||
disableNavigation()
|
||||
|
||||
// When you fill in the email address via auto-complete it adds extra whitespace
|
||||
let trimmedUsername = username.trimmingCharacters(in: .whitespaces)
|
||||
let credentials = Credentials(type: .newsBlurBasic, username: trimmedUsername, secret: password)
|
||||
Account.validateCredentials(type: .newsBlur, credentials: credentials) { result in
|
||||
|
||||
self.stopAnimtatingActivityIndicator()
|
||||
self.enableNavigation()
|
||||
|
||||
switch result {
|
||||
case .success(let credentials):
|
||||
if let credentials = credentials {
|
||||
var newAccount = false
|
||||
if self.account == nil {
|
||||
self.account = AccountManager.shared.createAccount(type: .newsBlur)
|
||||
newAccount = true
|
||||
}
|
||||
|
||||
do {
|
||||
|
||||
do {
|
||||
try self.account?.removeCredentials(type: .basic)
|
||||
} catch {}
|
||||
try self.account?.storeCredentials(credentials)
|
||||
|
||||
if newAccount {
|
||||
self.account?.refreshAll() { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
self.delegate?.dismiss()
|
||||
} catch {
|
||||
self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error"))
|
||||
}
|
||||
} else {
|
||||
self.showError(NSLocalizedString("Invalid username/password combination.", comment: "Credentials Error"))
|
||||
}
|
||||
case .failure(let error):
|
||||
self.showError(error.localizedDescription)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@objc func textDidChange(_ note: Notification) {
|
||||
actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false)
|
||||
}
|
||||
|
||||
private func showError(_ message: String) {
|
||||
presentError(title: "Error", message: message)
|
||||
}
|
||||
|
||||
private func enableNavigation() {
|
||||
self.cancelBarButtonItem.isEnabled = true
|
||||
self.actionButton.isEnabled = true
|
||||
}
|
||||
|
||||
private func disableNavigation() {
|
||||
cancelBarButtonItem.isEnabled = false
|
||||
actionButton.isEnabled = false
|
||||
}
|
||||
|
||||
private func startAnimatingActivityIndicator() {
|
||||
activityIndicator.isHidden = false
|
||||
activityIndicator.startAnimating()
|
||||
}
|
||||
|
||||
private func stopAnimtatingActivityIndicator() {
|
||||
self.activityIndicator.isHidden = true
|
||||
self.activityIndicator.stopAnimating()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension NewsBlurAccountViewController: UITextFieldDelegate {
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -35,6 +35,10 @@ struct AppAssets {
|
|||
return UIImage(named: "accountFreshRSS")!
|
||||
}()
|
||||
|
||||
static var accountNewsBlurImage: UIImage = {
|
||||
return UIImage(named: "accountNewsBlur")!
|
||||
}()
|
||||
|
||||
static var articleExtractorError: UIImage = {
|
||||
return UIImage(named: "articleExtractorError")!
|
||||
}()
|
||||
|
@ -238,6 +242,8 @@ struct AppAssets {
|
|||
return AppAssets.accountFeedWranglerImage
|
||||
case .freshRSS:
|
||||
return AppAssets.accountFreshRSSImage
|
||||
case .newsBlur:
|
||||
return AppAssets.accountNewsBlurImage
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "newsblur-512.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
|
@ -2,6 +2,16 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.$(ORGANIZATION_IDENTIFIER).NetNewsWire</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS</string>
|
||||
|
|
|
@ -56,6 +56,12 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
|||
let addViewController = navController.topViewController as! FeedWranglerAccountViewController
|
||||
addViewController.delegate = self
|
||||
present(navController, animated: true)
|
||||
case 4:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "NewsBlurAccountNavigationViewController") as! UINavigationController
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
let addViewController = navController.topViewController as! NewsBlurAccountViewController
|
||||
addViewController.delegate = self
|
||||
present(navController, animated: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
@ -677,6 +677,43 @@
|
|||
<outlet property="accountNameLabel" destination="Dur-Qf-YYi" id="DAF-c9-MJM"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="56" id="Btn-uu-2ks" customClass="SettingsAccountTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="241" width="374" height="56"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Btn-uu-2ks" id="rSE-Cm-Oom">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="56"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="PJ5-Pm-b2p">
|
||||
<rect key="frame" x="20" y="12" width="164" height="32"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="accountNewsBlur" translatesAutoresizingMaskIntoConstraints="NO" id="6Tf-XJ-1e0">
|
||||
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
|
||||
<color key="tintColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="32" id="Bhm-KX-Sch"/>
|
||||
<constraint firstAttribute="height" constant="32" id="sFc-DJ-NBg"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="NewsBlur" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lKr-Le-Atw">
|
||||
<rect key="frame" x="48" y="0.0" width="116" height="32"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="PJ5-Pm-b2p" firstAttribute="centerY" secondItem="rSE-Cm-Oom" secondAttribute="centerY" id="4Zs-Lm-lmM"/>
|
||||
<constraint firstItem="PJ5-Pm-b2p" firstAttribute="leading" secondItem="rSE-Cm-Oom" secondAttribute="leading" constant="20" symbolic="YES" id="tDb-Wo-OOG"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="accountImage" destination="6Tf-XJ-1e0" id="PGF-56-QEs"/>
|
||||
<outlet property="accountNameLabel" destination="lKr-Le-Atw" id="g8z-Fb-JVk"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
|
@ -1050,6 +1087,7 @@
|
|||
<image name="accountFeedbin" width="120" height="102"/>
|
||||
<image name="accountFeedly" width="138" height="123"/>
|
||||
<image name="accountLocal" width="99" height="77"/>
|
||||
<image name="accountNewsBlur" width="512" height="512"/>
|
||||
<namedColor name="primaryAccentColor">
|
||||
<color red="0.031372549019607843" green="0.41568627450980394" blue="0.93333333333333335" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
|
|
Loading…
Reference in New Issue