Implement status change
This commit is contained in:
parent
8f64f7230d
commit
e1d5288d3d
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB7399814F6FB3247825C /* NewsBlurStory.swift */; };
|
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 */; };
|
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */; };
|
||||||
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */; };
|
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */; };
|
||||||
179DBED55C9B4D6A413486C1 /* NewsBlurUnreadStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB818180A51098A9816B2 /* NewsBlurUnreadStory.swift */; };
|
179DBED55C9B4D6A413486C1 /* NewsBlurUnreadStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB818180A51098A9816B2 /* NewsBlurUnreadStory.swift */; };
|
||||||
|
@ -229,6 +230,7 @@
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurLoginResponse.swift; sourceTree = "<group>"; };
|
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>"; };
|
179DB1B909672E0E807B5E8C /* NewsBlurFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurFeed.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>"; };
|
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>"; };
|
179DB7399814F6FB3247825C /* NewsBlurStory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurStory.swift; sourceTree = "<group>"; };
|
||||||
179DB818180A51098A9816B2 /* NewsBlurUnreadStory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurUnreadStory.swift; sourceTree = "<group>"; };
|
179DB818180A51098A9816B2 /* NewsBlurUnreadStory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurUnreadStory.swift; sourceTree = "<group>"; };
|
||||||
|
@ -457,6 +459,7 @@
|
||||||
179DB7399814F6FB3247825C /* NewsBlurStory.swift */,
|
179DB7399814F6FB3247825C /* NewsBlurStory.swift */,
|
||||||
179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */,
|
179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */,
|
||||||
179DB818180A51098A9816B2 /* NewsBlurUnreadStory.swift */,
|
179DB818180A51098A9816B2 /* NewsBlurUnreadStory.swift */,
|
||||||
|
179DB5B421C5433B45C5F13E /* NewsBlurStoryStatusChange.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1151,6 +1154,7 @@
|
||||||
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */,
|
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */,
|
||||||
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */,
|
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */,
|
||||||
179DBED55C9B4D6A413486C1 /* NewsBlurUnreadStory.swift in Sources */,
|
179DBED55C9B4D6A413486C1 /* NewsBlurUnreadStory.swift in Sources */,
|
||||||
|
179DB0B17A6C51B95ABC1741 /* NewsBlurStoryStatusChange.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
var asData: Data? {
|
||||||
|
var postData = URLComponents()
|
||||||
|
postData.queryItems = hashes.map { URLQueryItem(name: "story_hash", value: $0) }
|
||||||
|
|
||||||
|
return postData.percentEncodedQuery?.data(using: .utf8)
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,11 +11,14 @@ import RSWeb
|
||||||
|
|
||||||
enum NewsBlurError: LocalizedError {
|
enum NewsBlurError: LocalizedError {
|
||||||
case general(message: String)
|
case general(message: String)
|
||||||
|
case unknown
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .general(let message):
|
case .general(let message):
|
||||||
return message
|
return message
|
||||||
|
case .unknown:
|
||||||
|
return "An unknown error occurred"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,4 +191,40 @@ final class NewsBlurAPICaller: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func markAsUnread(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
sendStoryStatus(endpoint: "reader/mark_story_hash_as_unread", hashes: hashes, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAsRead(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
sendStoryStatus(endpoint: "reader/mark_story_hashes_as_read", hashes: hashes, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func star(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
sendStoryStatus(endpoint: "reader/mark_story_hash_as_starred", hashes: hashes, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unstar(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
sendStoryStatus(endpoint: "reader/mark_story_hash_as_unstarred", hashes: hashes, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendStoryStatus(endpoint: String, hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
let callURL = baseURL.appendingPathComponent(endpoint)
|
||||||
|
|
||||||
|
var request = URLRequest(url: callURL, credentials: credentials)
|
||||||
|
request.httpBody = NewsBlurStoryStatusChange(hashes: hashes).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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,11 +116,74 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
func sendArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||||
completion(.success(()))
|
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(deleteStarredStatuses, 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>) -> ()) {
|
func refreshArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||||
completion(.success(()))
|
os_log(.debug, log: log, "Refreshing article statuses...")
|
||||||
|
|
||||||
|
// TODO: Fill this in
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshStories(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
func refreshStories(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
@ -192,7 +255,18 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
||||||
fatalError("markArticles(for:articles:statusKey:flag:) has not been implemented")
|
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) {
|
func accountDidInitialize(_ account: Account) {
|
||||||
|
@ -485,4 +559,43 @@ extension NewsBlurAccountDelegate {
|
||||||
|
|
||||||
return Set(parsedItems)
|
return Set(parsedItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private 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(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue