From 58575331dd66a4d8b498e34c6809cb9c95ef3193 Mon Sep 17 00:00:00 2001
From: Stuart Breckenridge <stuart.breckenridge@icloud.com>
Date: Wed, 13 May 2020 12:33:51 +0800
Subject: [PATCH] Fixes #2018
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This commit adds a new `MarkAsReadAlertControllerSourceType` protocol to which `CGRect`, `UIView`, and `UIBarButtonItem` conform to.

The `MarkAsReadAlertController` now presents an action sheet for mark as read, mark above as read, and mark below as read. The above is used for the `popoverPresentationController`’s `barButtonItem`, `sourceRect`, or `sourceView` as needed.
---
 iOS/MasterFeed/MasterFeedViewController.swift | 15 ++---
 .../MarkAsReadAlertController.swift           | 42 +++++++++++---
 .../MasterTimelineViewController.swift        | 56 ++++++++++---------
 3 files changed, 72 insertions(+), 41 deletions(-)

diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift
index d8f99402f..accf4b67e 100644
--- a/iOS/MasterFeed/MasterFeedViewController.swift
+++ b/iOS/MasterFeed/MasterFeedViewController.swift
@@ -937,7 +937,7 @@ private extension MasterFeedViewController {
 		guard let node = dataSource.itemIdentifier(for: indexPath),
 			coordinator.unreadCountFor(node) > 0,
 			let feed = node.representedObject as? WebFeed,
-			let articles = try? feed.fetchArticles() else {
+			let articles = try? feed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
 				return nil
 		}
 		
@@ -947,8 +947,9 @@ private extension MasterFeedViewController {
 			completion(true)
 		}
 		
+
 		let action = UIAlertAction(title: title, style: .default) { [weak self] action in
-			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in
+			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
 				self?.coordinator.markAllAsRead(Array(articles))
 				completion(true)
 			}
@@ -1026,7 +1027,7 @@ private extension MasterFeedViewController {
 		}
 
 		let articles = Array(fetchedArticles)
-		return markAllAsReadAction(articles: articles, nameForDisplay: articleFetcher.nameForDisplay)
+		return markAllAsReadAction(articles: articles, nameForDisplay: articleFetcher.nameForDisplay, indexPath: indexPath)
 	}
 
 	func markAllAsReadAction(account: Account) -> UIAction? {
@@ -1038,16 +1039,16 @@ private extension MasterFeedViewController {
 		return markAllAsReadAction(articles: articles, nameForDisplay: account.nameForDisplay)
 	}
 
-	func markAllAsReadAction(articles: [Article], nameForDisplay: String) -> UIAction? {
-		guard articles.canMarkAllAsRead() else {
+	func markAllAsReadAction(articles: [Article], nameForDisplay: String, indexPath: IndexPath? = nil) -> UIAction? {
+		guard articles.canMarkAllAsRead(), let indexPath = indexPath, let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
 			return nil
 		}
 
 		let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
 		let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, nameForDisplay) as String
-
+		
 		let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
-			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in
+			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
 				self?.coordinator.markAllAsRead(articles)
 			}
 		}
diff --git a/iOS/MasterTimeline/MarkAsReadAlertController.swift b/iOS/MasterTimeline/MarkAsReadAlertController.swift
index 670b2c637..241b734c2 100644
--- a/iOS/MasterTimeline/MarkAsReadAlertController.swift
+++ b/iOS/MasterTimeline/MarkAsReadAlertController.swift
@@ -9,13 +9,20 @@
 import Foundation
 import UIKit
 
+protocol MarkAsReadAlertControllerSourceType {}
+extension CGRect: MarkAsReadAlertControllerSourceType {}
+extension UIView: MarkAsReadAlertControllerSourceType {}
+extension UIBarButtonItem: MarkAsReadAlertControllerSourceType {}
+
+
 struct MarkAsReadAlertController {
 	
-	static func confirm(_ controller: UIViewController?,
-						coordinator: SceneCoordinator?,
-						confirmTitle: String,
-						cancelCompletion: (() -> Void)? = nil,
-						completion: @escaping () -> Void) {
+	static func confirm<T>(_ controller: UIViewController?,
+						   coordinator: SceneCoordinator?,
+						   confirmTitle: String,
+						   sourceType: T,
+						   cancelCompletion: (() -> Void)? = nil,
+						   completion: @escaping () -> Void) where T: MarkAsReadAlertControllerSourceType {
 		
 		guard let controller = controller, let coordinator = coordinator else {
 			completion()
@@ -23,7 +30,7 @@ struct MarkAsReadAlertController {
 		}
 		
 		if AppDefaults.confirmMarkAllAsRead {
-			let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion) { _ in
+			let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion, sourceType: sourceType) { _ in
 				completion()
 			}
 			controller.present(alertController, animated: true)
@@ -32,10 +39,12 @@ struct MarkAsReadAlertController {
 		}
 	}
 	
-	private static func alert(coordinator: SceneCoordinator,
+	private static func alert<T>(coordinator: SceneCoordinator,
 							  confirmTitle: String,
 							  cancelCompletion: (() -> Void)?,
-							  completion: @escaping (UIAlertAction) -> Void) -> UIAlertController {
+							  sourceType: T,
+							  completion: @escaping (UIAlertAction) -> Void) -> UIAlertController where T: MarkAsReadAlertControllerSourceType  {
+		
 		
 		let title = NSLocalizedString("Mark As Read", comment: "Mark As Read")
 		let message = NSLocalizedString("You can turn this confirmation off in settings.",
@@ -43,7 +52,7 @@ struct MarkAsReadAlertController {
 		let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
 		let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings")
 		
-		let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
+		let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
 		let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in
 			cancelCompletion?()
 		}
@@ -55,6 +64,21 @@ struct MarkAsReadAlertController {
 		alertController.addAction(markAction)
 		alertController.addAction(settingsAction)
 		alertController.addAction(cancelAction)
+		
+		if let barButtonItem = sourceType as? UIBarButtonItem {
+			alertController.popoverPresentationController?.barButtonItem = barButtonItem
+			return alertController
+		}
+		
+		if let rect = sourceType as? CGRect {
+			alertController.popoverPresentationController?.sourceRect = rect
+			return alertController
+		}
+		
+		if let view = sourceType as? UIView {
+			alertController.popoverPresentationController?.sourceView = view
+			return alertController
+		}
 
 		return alertController
 	}
diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift
index fdd148c81..54f1f8bb9 100644
--- a/iOS/MasterTimeline/MasterTimelineViewController.swift
+++ b/iOS/MasterTimeline/MasterTimelineViewController.swift
@@ -116,7 +116,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
 	
 	@IBAction func markAllAsRead(_ sender: Any) {
 		let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
-		MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title) { [weak self] in
+		
+		guard let barButtonItem = sender as? UIBarButtonItem else {
+			return
+		}
+		
+		MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: barButtonItem) { [weak self] in
 			self?.coordinator.markAllAsReadInTimeline()
 		}
 	}
@@ -260,11 +265,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
 					popoverController.sourceRect = CGRect(x: view.frame.size.width/2, y: view.frame.size.height/2, width: 1, height: 1)
 				}
 
-				if let action = self.markAboveAsReadAlertAction(article, completion: completion) {
+				if let action = self.markAboveAsReadAlertAction(article, indexPath: indexPath, completion: completion) {
 					alert.addAction(action)
 				}
 
-				if let action = self.markBelowAsReadAlertAction(article, completion: completion) {
+				if let action = self.markBelowAsReadAlertAction(article, indexPath: indexPath, completion: completion) {
 					alert.addAction(action)
 				}
 				
@@ -272,7 +277,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
 					alert.addAction(action)
 				}
 				
-				if let action = self.markAllInFeedAsReadAlertAction(article, completion: completion) {
+				if let action = self.markAllInFeedAsReadAlertAction(article, indexPath: indexPath, completion: completion) {
 					alert.addAction(action)
 				}
 
@@ -317,11 +322,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
 			
 			actions.append(self.toggleArticleStarStatusAction(article))
 
-			if let action = self.markAboveAsReadAction(article) {
+			if let action = self.markAboveAsReadAction(article, indexPath: indexPath) {
 				actions.append(action)
 			}
 
-			if let action = self.markBelowAsReadAction(article) {
+			if let action = self.markBelowAsReadAction(article, indexPath: indexPath) {
 				actions.append(action)
 			}
 			
@@ -329,7 +334,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
 				actions.append(action)
 			}
 			
-			if let action = self.markAllInFeedAsReadAction(article) {
+			if let action = self.markAllInFeedAsReadAction(article, indexPath: indexPath) {
 				actions.append(action)
 			}
 			
@@ -696,38 +701,38 @@ private extension MasterTimelineViewController {
 		return action
 	}
 
-	func markAboveAsReadAction(_ article: Article) -> UIAction? {
-		guard coordinator.canMarkAboveAsRead(for: article) else {
+	func markAboveAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
+		guard coordinator.canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
 			return nil
 		}
 
 		let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read")
 		let image = AppAssets.markAboveAsReadImage
 		let action = UIAction(title: title, image: image) { [weak self] action in
-			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in
+			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
 				self?.coordinator.markAboveAsRead(article)
 			}
 		}
 		return action
 	}
 	
-	func markBelowAsReadAction(_ article: Article) -> UIAction? {
-		guard coordinator.canMarkBelowAsRead(for: article) else {
+	func markBelowAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
+		guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
 			return nil
 		}
 
 		let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read")
 		let image = AppAssets.markBelowAsReadImage
 		let action = UIAction(title: title, image: image) { [weak self] action in
-			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in
+			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
 				self?.coordinator.markBelowAsRead(article)
 			}
 		}
 		return action
 	}
 	
-	func markAboveAsReadAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
-		guard coordinator.canMarkAboveAsRead(for: article) else {
+	func markAboveAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
+		guard coordinator.canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
 			return nil
 		}
 
@@ -737,7 +742,7 @@ private extension MasterTimelineViewController {
 		}
 
 		let action = UIAlertAction(title: title, style: .default) { [weak self] action in
-			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in
+			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
 				self?.coordinator.markAboveAsRead(article)
 				completion(true)
 			}
@@ -745,8 +750,8 @@ private extension MasterTimelineViewController {
 		return action
 	}
 
-	func markBelowAsReadAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
-		guard coordinator.canMarkBelowAsRead(for: article) else {
+	func markBelowAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
+		guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
 			return nil
 		}
 
@@ -756,7 +761,7 @@ private extension MasterTimelineViewController {
 		}
 		
 		let action = UIAlertAction(title: title, style: .default) { [weak self] action in
-			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in
+			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
 				self?.coordinator.markBelowAsRead(article)
 				completion(true)
 			}
@@ -787,36 +792,37 @@ private extension MasterTimelineViewController {
 		return action
 	}
 	
-	func markAllInFeedAsReadAction(_ article: Article) -> UIAction? {
+	func markAllInFeedAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
 		guard let webFeed = article.webFeed else { return nil }
 		guard let fetchedArticles = try? webFeed.fetchArticles() else {
 			return nil
 		}
 
 		let articles = Array(fetchedArticles)
-		guard articles.canMarkAllAsRead() else {
+		guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
 			return nil
 		}
 		
+		
 		let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
 		let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
 		
 		let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
-			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title) { [weak self] in
+			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
 				self?.coordinator.markAllAsRead(articles)
 			}
 		}
 		return action
 	}
 
-	func markAllInFeedAsReadAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
+	func markAllInFeedAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
 		guard let webFeed = article.webFeed else { return nil }
 		guard let fetchedArticles = try? webFeed.fetchArticles() else {
 			return nil
 		}
 		
 		let articles = Array(fetchedArticles)
-		guard articles.canMarkAllAsRead() else {
+		guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
 			return nil
 		}
 		
@@ -827,7 +833,7 @@ private extension MasterTimelineViewController {
 		}
 		
 		let action = UIAlertAction(title: title, style: .default) { [weak self] action in
-			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, cancelCompletion: cancel) { [weak self] in
+			MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
 				self?.coordinator.markAllAsRead(articles)
 				completion(true)
 			}