diff --git a/WooCommerce/Classes/ViewRelated/Notifications/NotificationDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Notifications/NotificationDetailsViewController.swift index 2c5b080f84a..4effdf5e258 100644 --- a/WooCommerce/Classes/ViewRelated/Notifications/NotificationDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Notifications/NotificationDetailsViewController.swift @@ -17,11 +17,19 @@ class NotificationDetailsViewController: UIViewController { return EntityListener(storageManager: AppDelegate.shared.storageManager, readOnlyEntity: note) }() + /// Pull To Refresh Support. + /// + private lazy var refreshControl: UIRefreshControl = { + let refreshControl = UIRefreshControl() + refreshControl.addTarget(self, action: #selector(pullToRefresh(sender:)), for: .valueChanged) + return refreshControl + }() + /// Note to be displayed! /// private var note: Note! { didSet { - buildDetailsRows() + reloadInterface() } } @@ -57,7 +65,7 @@ class NotificationDetailsViewController: UIViewController { configureEntityListener() registerTableViewCells() - buildDetailsRows() + reloadInterface() } } @@ -69,8 +77,6 @@ private extension NotificationDetailsViewController { /// Setup: Navigation /// func configureNavigationItem() { - title = note.title - // Don't show the Notifications title in the next-view's back button navigationItem.backBarButtonItem = UIBarButtonItem(title: String(), style: .plain, target: nil, action: nil) } @@ -87,6 +93,7 @@ private extension NotificationDetailsViewController { // Hide "Empty Rows" tableView.tableFooterView = UIView() tableView.backgroundColor = StyleManager.tableViewBackgroundColor + tableView.refreshControl = refreshControl } /// Setup: EntityListener @@ -114,13 +121,44 @@ private extension NotificationDetailsViewController { } +// MARK: - Sync +// +private extension NotificationDetailsViewController { + + /// Refresh Control's Callback. + /// + @IBAction func pullToRefresh(sender: UIRefreshControl) { + WooAnalytics.shared.track(.notificationsListPulledToRefresh) + + synchronizeNotification(noteId: note.noteId) { + sender.endRefreshing() + } + } + + /// Synchronizes the Notifications associated to the active WordPress.com account. + /// + func synchronizeNotification(noteId: Int64, onCompletion: @escaping () -> Void) { + let action = NotificationAction.synchronizeNotification(noteId: noteId) { error in + if let error = error { + DDLogError("⛔️ Error synchronizing notification [\(noteId)]: \(error)") + } + + onCompletion() + } + + StoresManager.shared.dispatch(action) + } +} + + // MARK: - Private Methods // private extension NotificationDetailsViewController { - /// Reloads all of the Notification Detail Rows! + /// Reloads all of the Details Interface /// - func buildDetailsRows() { + func reloadInterface() { + title = note.title rows = NoteDetailsRow.details(from: note) tableView.reloadData() } diff --git a/Yosemite/Yosemite/Actions/NotificationAction.swift b/Yosemite/Yosemite/Actions/NotificationAction.swift index ef306a21d4b..166f6afc632 100644 --- a/Yosemite/Yosemite/Actions/NotificationAction.swift +++ b/Yosemite/Yosemite/Actions/NotificationAction.swift @@ -7,6 +7,7 @@ import Networking // public enum NotificationAction: Action { case synchronizeNotifications(onCompletion: (Error?) -> Void) + case synchronizeNotification(noteId: Int64, onCompletion: (Error?) -> Void) case updateLastSeen(timestamp: String, onCompletion: (Error?) -> Void) case updateReadStatus(noteID: Int64, read: Bool, onCompletion: (Error?) -> Void) } diff --git a/Yosemite/Yosemite/Model/ReadOnly/Note+ReadOnlyType.swift b/Yosemite/Yosemite/Model/ReadOnly/Note+ReadOnlyType.swift index 7e4165ebbcb..504d07aa817 100644 --- a/Yosemite/Yosemite/Model/ReadOnly/Note+ReadOnlyType.swift +++ b/Yosemite/Yosemite/Model/ReadOnly/Note+ReadOnlyType.swift @@ -13,7 +13,6 @@ extension Yosemite.Note: ReadOnlyType { return false } - return storageNote.noteID == noteId && - storageNote.noteHash == hash + return storageNote.noteID == noteId } } diff --git a/Yosemite/Yosemite/Stores/NotificationStore.swift b/Yosemite/Yosemite/Stores/NotificationStore.swift index 3e99205ceff..48e077ded1c 100644 --- a/Yosemite/Yosemite/Stores/NotificationStore.swift +++ b/Yosemite/Yosemite/Stores/NotificationStore.swift @@ -32,6 +32,8 @@ public class NotificationStore: Store { switch action { case .synchronizeNotifications(let onCompletion): synchronizeNotifications(onCompletion: onCompletion) + case .synchronizeNotification(let noteId, let onCompletion): + synchronizeNotification(with: noteId, onCompletion: onCompletion) case .updateLastSeen(let timestamp, let onCompletion): updateLastSeen(timestamp: timestamp, onCompletion: onCompletion) case .updateReadStatus(let noteID, let read, let onCompletion): @@ -81,7 +83,28 @@ private extension NotificationStore { } } - + + /// Synchronizes the Notification matching the specified ID, and updates the local entity. + /// + /// - Parameters: + /// - noteId: Notification ID of the note to be downloaded. + /// - onCompletion: Closure to be executed on completion. + /// + func synchronizeNotification(with noteId: Int64, onCompletion: @escaping (Error?) -> Void) { + let remote = NotificationsRemote(network: network) + + remote.loadNotes(noteIds: [noteId]) { notes, error in + guard let notes = notes else { + onCompletion(error) + return + } + + self.updateLocalNotes(with: notes) { + onCompletion(nil) + } + } + } + /// Updates the last seen notification /// @@ -92,6 +115,7 @@ private extension NotificationStore { } } + /// Updates the read status for the given notification ID /// func updateReadStatus(noteID: Int64, read: Bool, onCompletion: @escaping (Error?) -> Void) { diff --git a/Yosemite/YosemiteTests/Stores/NotificationStoreTests.swift b/Yosemite/YosemiteTests/Stores/NotificationStoreTests.swift index 02741dbab71..99e498ddcc0 100644 --- a/Yosemite/YosemiteTests/Stores/NotificationStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/NotificationStoreTests.swift @@ -111,7 +111,6 @@ class NotificationStoreTests: XCTestCase { /// Initial Sync /// let initialSyncAction = NotificationAction.synchronizeNotifications() { (error) in - XCTAssertEqual(self.viewStorage.countObjects(ofType: Storage.Note.self), 40) notificationStore.onAction(nestedSyncAction) } @@ -120,6 +119,33 @@ class NotificationStoreTests: XCTestCase { wait(for: [expectation], timeout: Constants.expectationTimeout) } + /// Verifies that `NotificationAction.synchronizeNotification` will effectively request a single notification, + /// which will be stored in CoreData. + /// + func testSynchronizeSingleNotificationEffectivelyUpdatesRequestedNote() { + let expectation = self.expectation(description: "Sync notification") + let notificationStore = NotificationStore(dispatcher: dispatcher, storageManager: storageManager, network: network) + let notificationId = Int64(100001) + + network.simulateResponse(requestUrlSuffix: "notifications", filename: "notifications-load-all") + XCTAssertEqual(viewStorage.countObjects(ofType: Storage.Note.self), 0) + + let syncAction = NotificationAction.synchronizeNotification(noteId: notificationId) { error in + let note = self.viewStorage.loadNotification(noteID: notificationId) + XCTAssertNil(error) + XCTAssertNotNil(note) + + let request = self.network.requestsForResponseData[0] as! DotcomRequest + XCTAssertEqual(request.parameters?["ids"], String(notificationId)) + + expectation.fulfill() + } + + notificationStore.onAction(syncAction) + wait(for: [expectation], timeout: Constants.expectationTimeout) + } + + // MARK: - NotificationAction.updateLastSeen /// Verifies that NotificationAction.updateLastSeen handles a success response from the backend properly