Implement status change

This commit is contained in:
Anh Do 2020-03-13 23:16:52 -04:00
parent 8f64f7230d
commit e1d5288d3d
No known key found for this signature in database
GPG Key ID: 451E3092F917B62D
4 changed files with 181 additions and 3 deletions

View File

@ -8,6 +8,7 @@
/* 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 */; };
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */; };
179DBED55C9B4D6A413486C1 /* NewsBlurUnreadStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB818180A51098A9816B2 /* NewsBlurUnreadStory.swift */; };
@ -229,6 +230,7 @@
/* 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>"; };
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>"; };
179DB818180A51098A9816B2 /* NewsBlurUnreadStory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurUnreadStory.swift; sourceTree = "<group>"; };
@ -457,6 +459,7 @@
179DB7399814F6FB3247825C /* NewsBlurStory.swift */,
179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */,
179DB818180A51098A9816B2 /* NewsBlurUnreadStory.swift */,
179DB5B421C5433B45C5F13E /* NewsBlurStoryStatusChange.swift */,
);
path = Models;
sourceTree = "<group>";
@ -1151,6 +1154,7 @@
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */,
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */,
179DBED55C9B4D6A413486C1 /* NewsBlurUnreadStory.swift in Sources */,
179DB0B17A6C51B95ABC1741 /* NewsBlurStoryStatusChange.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -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)
}
}

View File

@ -11,11 +11,14 @@ import RSWeb
enum NewsBlurError: LocalizedError {
case general(message: String)
case unknown
var errorDescription: String? {
switch self {
case .general(let 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))
}
}
}
}

View File

@ -116,11 +116,74 @@ final class NewsBlurAccountDelegate: AccountDelegate {
}
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(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>) -> ()) {
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) {
@ -192,7 +255,18 @@ final class NewsBlurAccountDelegate: AccountDelegate {
}
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) {
@ -485,4 +559,43 @@ extension NewsBlurAccountDelegate {
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(()))
}
}
}
}