2019-04-15 22:03:05 +02:00
//
// M a s t e r T i m e l i n e V i e w C o n t r o l l e r . s w i f t
// N e t N e w s W i r e
//
// C r e a t e d b y M a u r i c e P a r k e r o n 4 / 8 / 1 9 .
// C o p y r i g h t © 2 0 1 9 R a n c h e r o S o f t w a r e . A l l r i g h t s r e s e r v e d .
//
import UIKit
import RSCore
import Account
import Articles
2019-07-27 21:49:07 +02:00
class MasterTimelineViewController : UITableViewController , UndoableCommandRunner {
2019-04-17 01:08:02 +02:00
2019-04-29 22:29:00 +02:00
private var numberOfTextLines = 0
2019-11-17 02:44:01 +01:00
private var iconSize = IconSize . medium
2019-11-24 17:47:09 +01:00
private lazy var feedTapGestureRecognizer = UITapGestureRecognizer ( target : self , action : #selector ( showFeedInspector ( _ : ) ) )
2019-04-15 22:03:05 +02:00
2020-01-03 22:23:37 +01:00
private var refreshProgressView : RefreshProgressView ?
2021-02-01 01:12:20 +01:00
@IBOutlet weak var markAllAsReadButton : UIBarButtonItem ! {
didSet {
if #available ( iOS 14 , * ) {
markAllAsReadButton . primaryAction = nil
} else {
markAllAsReadButton . action = #selector ( MasterTimelineViewController . markAllAsRead ( _ : ) )
}
}
}
2020-08-07 22:20:20 +02:00
private var filterButton : UIBarButtonItem !
private var firstUnreadButton : UIBarButtonItem !
2019-08-30 21:17:05 +02:00
private lazy var dataSource = makeDataSource ( )
2019-08-31 18:50:34 +02:00
private let searchController = UISearchController ( searchResultsController : nil )
2019-09-01 19:43:07 +02:00
weak var coordinator : SceneCoordinator !
2019-04-21 20:57:23 +02:00
var undoableCommands = [ UndoableCommand ] ( )
2020-01-21 19:05:47 +01:00
let scrollPositionQueue = CoalescingQueue ( name : " Timeline Scroll Position " , interval : 0.3 , maxInterval : 1.0 )
2019-08-31 18:50:34 +02:00
2019-09-05 21:37:07 +02:00
private let keyboardManager = KeyboardManager ( type : . timeline )
2019-09-04 23:24:16 +02:00
override var keyCommands : [ UIKeyCommand ] ? {
return keyboardManager . keyCommands
}
2019-04-17 01:08:02 +02:00
override var canBecomeFirstResponder : Bool {
return true
}
2019-09-04 23:24:16 +02:00
2019-04-15 22:03:05 +02:00
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2019-08-30 21:17:05 +02:00
2019-04-23 11:35:48 +02:00
NotificationCenter . default . addObserver ( self , selector : #selector ( unreadCountDidChange ( _ : ) ) , name : . UnreadCountDidChange , object : nil )
2019-04-15 22:03:05 +02:00
NotificationCenter . default . addObserver ( self , selector : #selector ( statusesDidChange ( _ : ) ) , name : . StatusesDidChange , object : nil )
2019-11-15 03:11:41 +01:00
NotificationCenter . default . addObserver ( self , selector : #selector ( webFeedIconDidBecomeAvailable ( _ : ) ) , name : . WebFeedIconDidBecomeAvailable , object : nil )
2019-04-15 22:03:05 +02:00
NotificationCenter . default . addObserver ( self , selector : #selector ( avatarDidBecomeAvailable ( _ : ) ) , name : . AvatarDidBecomeAvailable , object : nil )
2019-08-22 02:37:19 +02:00
NotificationCenter . default . addObserver ( self , selector : #selector ( faviconDidBecomeAvailable ( _ : ) ) , name : . FaviconDidBecomeAvailable , object : nil )
2019-04-29 22:29:00 +02:00
NotificationCenter . default . addObserver ( self , selector : #selector ( userDefaultsDidChange ( _ : ) ) , name : UserDefaults . didChangeNotification , object : nil )
2019-04-29 21:50:56 +02:00
NotificationCenter . default . addObserver ( self , selector : #selector ( contentSizeCategoryDidChange ) , name : UIContentSizeCategory . didChangeNotification , object : nil )
2019-09-28 02:45:09 +02:00
NotificationCenter . default . addObserver ( self , selector : #selector ( displayNameDidChange ) , name : . DisplayNameDidChange , object : nil )
2020-01-07 05:23:39 +01:00
NotificationCenter . default . addObserver ( self , selector : #selector ( willEnterForeground ( _ : ) ) , name : UIApplication . willEnterForegroundNotification , object : nil )
2019-09-28 02:45:09 +02:00
2020-08-07 22:20:20 +02:00
// I n i t i a l i z e P r o g r a m m a t i c B u t t o n s
filterButton = UIBarButtonItem ( image : AppAssets . filterInactiveImage , style : . plain , target : self , action : #selector ( toggleFilter ( _ : ) ) )
firstUnreadButton = UIBarButtonItem ( image : AppAssets . nextUnreadArticleImage , style : . plain , target : self , action : #selector ( firstUnread ( _ : ) ) )
2019-10-29 03:12:09 +01:00
// S e t u p t h e S e a r c h C o n t r o l l e r
searchController . delegate = self
searchController . searchResultsUpdater = self
searchController . obscuresBackgroundDuringPresentation = false
searchController . searchBar . delegate = self
searchController . searchBar . placeholder = NSLocalizedString ( " Search Articles " , comment : " Search Articles " )
searchController . searchBar . scopeButtonTitles = [
NSLocalizedString ( " Here " , comment : " Here " ) ,
NSLocalizedString ( " All Articles " , comment : " All Articles " )
]
2019-11-02 17:49:44 +01:00
navigationItem . searchController = searchController
2019-10-29 03:12:09 +01:00
definesPresentationContext = true
2019-08-31 18:50:34 +02:00
// C o n f i g u r e t h e t a b l e
tableView . dataSource = dataSource
2020-07-02 04:47:45 +02:00
numberOfTextLines = AppDefaults . shared . timelineNumberOfLines
iconSize = AppDefaults . shared . timelineIconSize
2020-03-18 21:55:33 +01:00
resetEstimatedRowHeight ( )
2019-04-29 22:29:00 +02:00
2019-11-24 17:47:09 +01:00
if let titleView = Bundle . main . loadNibNamed ( " MasterTimelineTitleView " , owner : self , options : nil ) ? [ 0 ] as ? MasterTimelineTitleView {
navigationItem . titleView = titleView
}
2020-01-03 22:23:37 +01:00
refreshControl = UIRefreshControl ( )
refreshControl ! . addTarget ( self , action : #selector ( refreshAccounts ( _ : ) ) , for : . valueChanged )
configureToolbar ( )
2019-12-09 02:14:33 +01:00
resetUI ( resetScroll : true )
2019-11-11 23:59:42 +01:00
2019-12-25 01:34:47 +01:00
// L o a d t h e t a b l e a n d t h e n s c r o l l t o t h e s a v e d p o s i t i o n i f a v a i l a b l e
applyChanges ( animated : false ) {
if let restoreIndexPath = self . coordinator . timelineMiddleIndexPath {
self . tableView . scrollToRow ( at : restoreIndexPath , at : . middle , animated : false )
}
2019-11-11 23:59:42 +01:00
}
2020-05-20 15:32:19 +02:00
// D i s a b l e s w i p e b a c k o n i P a d M i c e
if #available ( iOS 13.4 , * ) {
guard let gesture = self . navigationController ? . interactivePopGestureRecognizer as ? UIPanGestureRecognizer else {
return
}
gesture . allowedScrollTypesMask = [ ]
}
2019-11-12 03:45:14 +01:00
}
2019-11-24 01:00:51 +01:00
override func viewWillAppear ( _ animated : Bool ) {
// I f t h e n a v b a r i s h i d d e n , f a d e i t i n t o a v o i d i t s h o w i n g s t u f f a s i t i s g e t t i n g l a i d o u t
if navigationController ? . navigationBar . isHidden ? ? false {
navigationController ? . navigationBar . alpha = 0
}
}
2019-11-19 02:12:24 +01:00
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( true )
coordinator . isTimelineViewControllerPending = false
2019-11-24 01:00:51 +01:00
if navigationController ? . navigationBar . alpha = = 0 {
UIView . animate ( withDuration : 0.5 ) {
self . navigationController ? . navigationBar . alpha = 1
}
}
2019-11-19 02:12:24 +01:00
}
2019-09-05 04:06:29 +02:00
// MARK: A c t i o n s
2019-11-22 01:22:43 +01:00
@IBAction func toggleFilter ( _ sender : Any ) {
2020-03-22 16:18:07 +01:00
coordinator . toggleReadArticlesFilter ( )
2019-11-22 01:22:43 +01:00
}
2019-04-15 22:03:05 +02:00
@IBAction func markAllAsRead ( _ sender : Any ) {
2020-01-11 19:30:16 +01:00
let title = NSLocalizedString ( " Mark All as Read " , comment : " Mark All as Read " )
2020-05-13 06:33:51 +02:00
2020-05-14 11:10:55 +02:00
if let source = sender as ? UIBarButtonItem {
MarkAsReadAlertController . confirm ( self , coordinator : coordinator , confirmTitle : title , sourceType : source ) { [ weak self ] in
self ? . coordinator . markAllAsReadInTimeline ( )
}
2020-05-13 06:33:51 +02:00
}
2020-05-14 11:10:55 +02:00
if let _ = sender as ? UIKeyCommand {
guard let indexPath = tableView . indexPathForSelectedRow , let contentView = tableView . cellForRow ( at : indexPath ) ? . contentView else {
return
}
MarkAsReadAlertController . confirm ( self , coordinator : coordinator , confirmTitle : title , sourceType : contentView ) { [ weak self ] in
self ? . coordinator . markAllAsReadInTimeline ( )
}
2019-04-15 22:03:05 +02:00
}
}
2019-04-23 11:35:48 +02:00
@IBAction func firstUnread ( _ sender : Any ) {
2019-10-10 04:39:11 +02:00
coordinator . selectFirstUnread ( )
2019-04-23 01:00:26 +02:00
}
2020-01-03 22:23:37 +01:00
@objc func refreshAccounts ( _ sender : Any ) {
refreshControl ? . endRefreshing ( )
2020-01-11 02:14:21 +01:00
2020-01-03 22:23:37 +01:00
// T h i s i s a h a c k t o m a k e s u r e t h a t a n e r r o r d i a l o g d o e s n ' t i n t e r f e r e w i t h d i s m i s s i n g t h e r e f r e s h C o n t r o l .
// I f t h e e r r o r d i a l o g a p p e a r s t o o c l o s e l y t o t h e c a l l t o e n d R e f r e s h i n g , t h e n t h e r e f r e s h C o n t r o l n e v e r d i s a p p e a r s .
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.5 ) {
2020-03-11 21:47:00 +01:00
appDelegate . manualRefresh ( errorHandler : ErrorHandler . present ( self ) )
2020-01-03 22:23:37 +01:00
}
}
2019-09-05 04:06:29 +02:00
// MARK: K e y b o a r d s h o r t c u t s
@objc func selectNextUp ( _ sender : Any ? ) {
coordinator . selectPrevArticle ( )
}
@objc func selectNextDown ( _ sender : Any ? ) {
coordinator . selectNextArticle ( )
}
@objc func navigateToSidebar ( _ sender : Any ? ) {
coordinator . navigateToFeeds ( )
}
@objc func navigateToDetail ( _ sender : Any ? ) {
coordinator . navigateToDetail ( )
}
2020-05-26 06:51:42 +02:00
@objc func showFeedInspector ( _ sender : Any ? ) {
2019-09-28 02:45:09 +02:00
coordinator . showFeedInspector ( )
}
2020-05-13 13:59:59 +02:00
2019-08-25 18:38:04 +02:00
// MARK: A P I
2019-09-29 22:53:50 +02:00
func restoreSelectionIfNecessary ( adjustScroll : Bool ) {
2019-09-11 16:11:33 +02:00
if let article = coordinator . currentArticle , let indexPath = dataSource . indexPath ( for : article ) {
2019-09-29 22:53:50 +02:00
if adjustScroll {
2020-01-28 05:57:52 +01:00
tableView . selectRowAndScrollIfNotVisible ( at : indexPath , animations : [ ] )
2019-09-29 22:53:50 +02:00
} else {
tableView . selectRow ( at : indexPath , animated : false , scrollPosition : . none )
}
2019-09-10 15:06:43 +02:00
}
}
2019-12-09 02:14:33 +01:00
func reinitializeArticles ( resetScroll : Bool ) {
resetUI ( resetScroll : resetScroll )
2019-08-25 18:38:04 +02:00
}
2019-11-19 18:16:43 +01:00
func reloadArticles ( animated : Bool ) {
applyChanges ( animated : animated )
2019-08-25 18:38:04 +02:00
}
2020-01-28 05:57:52 +01:00
func updateArticleSelection ( animations : Animations ) {
2019-09-11 16:11:33 +02:00
if let article = coordinator . currentArticle , let indexPath = dataSource . indexPath ( for : article ) {
2019-08-25 18:38:04 +02:00
if tableView . indexPathForSelectedRow != indexPath {
2020-01-28 05:57:52 +01:00
tableView . selectRowAndScrollIfNotVisible ( at : indexPath , animations : animations )
2019-08-25 18:38:04 +02:00
}
2019-09-05 04:06:29 +02:00
} else {
2020-01-28 05:57:52 +01:00
tableView . selectRow ( at : nil , animated : animations . contains ( . select ) , scrollPosition : . none )
2019-08-25 18:38:04 +02:00
}
2019-09-06 17:38:02 +02:00
2019-08-25 18:38:04 +02:00
updateUI ( )
}
2020-03-13 22:03:42 +01:00
func updateUI ( ) {
2020-05-10 17:00:04 +02:00
refreshProgressView ? . update ( )
2020-03-13 22:03:42 +01:00
updateTitleUnreadCount ( )
updateToolbar ( )
}
2020-01-16 01:53:12 +01:00
func hideSearch ( ) {
navigationItem . searchController ? . isActive = false
}
2019-08-25 18:38:04 +02:00
2019-09-02 00:41:46 +02:00
func showSearchAll ( ) {
navigationItem . searchController ? . isActive = true
navigationItem . searchController ? . searchBar . selectedScopeButtonIndex = 1
2019-09-06 17:29:00 +02:00
navigationItem . searchController ? . searchBar . becomeFirstResponder ( )
2019-09-02 00:41:46 +02:00
}
2019-09-05 04:06:29 +02:00
func focus ( ) {
becomeFirstResponder ( )
}
2019-04-15 22:03:05 +02:00
// MARK: - T a b l e v i e w
2019-10-03 22:55:16 +02:00
override func tableView ( _ tableView : UITableView , leadingSwipeActionsConfigurationForRowAt indexPath : IndexPath ) -> UISwipeActionsConfiguration ? {
2019-09-11 16:11:33 +02:00
guard let article = dataSource . itemIdentifier ( for : indexPath ) else { return nil }
2020-02-18 22:49:29 +01:00
guard ! article . status . read || article . isAvailableToMarkUnread else { return nil }
2019-04-29 13:01:53 +02:00
// S e t u p t h e r e a d a c t i o n
let readTitle = article . status . read ?
NSLocalizedString ( " Unread " , comment : " Unread " ) :
NSLocalizedString ( " Read " , comment : " Read " )
2019-04-15 22:03:05 +02:00
2019-12-15 01:14:55 +01:00
let readAction = UIContextualAction ( style : . normal , title : readTitle ) { [ weak self ] ( action , view , completion ) in
2019-09-11 16:11:33 +02:00
self ? . coordinator . toggleRead ( article )
2019-12-15 01:14:55 +01:00
completion ( true )
2019-04-15 22:03:05 +02:00
}
2019-09-18 09:57:32 +02:00
readAction . image = article . status . read ? AppAssets . circleClosedImage : AppAssets . circleOpenImage
2019-09-18 09:49:57 +02:00
readAction . backgroundColor = AppAssets . primaryAccentColor
2019-04-15 22:03:05 +02:00
2019-10-03 22:55:16 +02:00
return UISwipeActionsConfiguration ( actions : [ readAction ] )
}
override func tableView ( _ tableView : UITableView , trailingSwipeActionsConfigurationForRowAt indexPath : IndexPath ) -> UISwipeActionsConfiguration ? {
guard let article = dataSource . itemIdentifier ( for : indexPath ) else { return nil }
2019-04-29 13:01:53 +02:00
// S e t u p t h e s t a r a c t i o n
let starTitle = article . status . starred ?
NSLocalizedString ( " Unstar " , comment : " Unstar " ) :
NSLocalizedString ( " Star " , comment : " Star " )
2019-04-15 22:03:05 +02:00
2019-12-15 01:14:55 +01:00
let starAction = UIContextualAction ( style : . normal , title : starTitle ) { [ weak self ] ( action , view , completion ) in
2019-09-11 16:11:33 +02:00
self ? . coordinator . toggleStar ( article )
2019-12-15 01:14:55 +01:00
completion ( true )
2019-04-15 22:03:05 +02:00
}
2019-09-18 09:57:32 +02:00
starAction . image = article . status . starred ? AppAssets . starOpenImage : AppAssets . starClosedImage
2019-04-29 13:01:53 +02:00
starAction . backgroundColor = AppAssets . starColor
2019-04-15 22:03:05 +02:00
2019-08-19 00:34:53 +02:00
// S e t u p t h e r e a d a c t i o n
let moreTitle = NSLocalizedString ( " More " , comment : " More " )
2019-12-15 01:14:55 +01:00
let moreAction = UIContextualAction ( style : . normal , title : moreTitle ) { [ weak self ] ( action , view , completion ) in
2019-08-19 00:34:53 +02:00
if let self = self {
let alert = UIAlertController ( title : nil , message : nil , preferredStyle : . actionSheet )
if let popoverController = alert . popoverPresentationController {
popoverController . sourceView = view
popoverController . sourceRect = CGRect ( x : view . frame . size . width / 2 , y : view . frame . size . height / 2 , width : 1 , height : 1 )
}
2020-01-03 08:16:55 +01:00
2020-05-13 06:33:51 +02:00
if let action = self . markAboveAsReadAlertAction ( article , indexPath : indexPath , completion : completion ) {
2020-01-03 08:16:55 +01:00
alert . addAction ( action )
}
2020-05-13 06:33:51 +02:00
if let action = self . markBelowAsReadAlertAction ( article , indexPath : indexPath , completion : completion ) {
2020-01-03 08:16:55 +01:00
alert . addAction ( action )
}
2019-08-19 00:34:53 +02:00
2019-12-15 01:14:55 +01:00
if let action = self . discloseFeedAlertAction ( article , completion : completion ) {
2019-08-19 22:45:52 +02:00
alert . addAction ( action )
}
2020-05-13 06:33:51 +02:00
if let action = self . markAllInFeedAsReadAlertAction ( article , indexPath : indexPath , completion : completion ) {
2019-08-20 00:26:09 +02:00
alert . addAction ( action )
}
2019-12-15 01:14:55 +01:00
if let action = self . openInBrowserAlertAction ( article , completion : completion ) {
2019-08-20 00:38:30 +02:00
alert . addAction ( action )
}
2019-12-15 01:14:55 +01:00
if let action = self . shareAlertAction ( article , indexPath : indexPath , completion : completion ) {
2019-08-20 01:09:38 +02:00
alert . addAction ( action )
}
2019-08-19 00:34:53 +02:00
let cancelTitle = NSLocalizedString ( " Cancel " , comment : " Cancel " )
alert . addAction ( UIAlertAction ( title : cancelTitle , style : . cancel ) { _ in
2019-12-15 01:14:55 +01:00
completion ( true )
2019-08-19 00:34:53 +02:00
} )
self . present ( alert , animated : true )
}
}
2019-08-19 22:45:52 +02:00
moreAction . image = AppAssets . moreImage
2019-08-19 00:34:53 +02:00
moreAction . backgroundColor = UIColor . systemGray
2019-10-03 22:55:16 +02:00
return UISwipeActionsConfiguration ( actions : [ starAction , moreAction ] )
2019-04-15 22:03:05 +02:00
}
2019-08-16 20:19:06 +02:00
override func tableView ( _ tableView : UITableView , contextMenuConfigurationForRowAt indexPath : IndexPath , point : CGPoint ) -> UIContextMenuConfiguration ? {
2019-09-11 16:11:33 +02:00
guard let article = dataSource . itemIdentifier ( for : indexPath ) else { return nil }
2020-01-08 00:36:32 +01:00
return UIContextMenuConfiguration ( identifier : indexPath . row as NSCopying , previewProvider : nil , actionProvider : { [ weak self ] suggestedActions in
guard let self = self else { return nil }
2020-11-13 12:23:04 +01:00
var menuElements = [ UIMenuElement ] ( )
var markActions = [ UIAction ] ( )
2020-02-18 22:49:29 +01:00
if let action = self . toggleArticleReadStatusAction ( article ) {
2020-11-13 12:23:04 +01:00
markActions . append ( action )
2020-02-18 22:49:29 +01:00
}
2020-11-13 12:23:04 +01:00
markActions . append ( self . toggleArticleStarStatusAction ( article ) )
2020-05-13 06:33:51 +02:00
if let action = self . markAboveAsReadAction ( article , indexPath : indexPath ) {
2020-11-13 12:23:04 +01:00
markActions . append ( action )
2019-08-20 01:09:38 +02:00
}
2020-05-13 06:33:51 +02:00
if let action = self . markBelowAsReadAction ( article , indexPath : indexPath ) {
2020-11-13 12:23:04 +01:00
markActions . append ( action )
2020-01-08 00:36:32 +01:00
}
2020-11-13 12:23:04 +01:00
menuElements . append ( UIMenu ( title : " " , options : . displayInline , children : markActions ) )
2020-01-08 00:36:32 +01:00
2020-11-13 12:23:04 +01:00
var secondaryActions = [ UIAction ] ( )
2020-01-08 00:36:32 +01:00
if let action = self . discloseFeedAction ( article ) {
2020-11-13 12:23:04 +01:00
secondaryActions . append ( action )
2020-01-08 00:36:32 +01:00
}
2020-05-13 06:33:51 +02:00
if let action = self . markAllInFeedAsReadAction ( article , indexPath : indexPath ) {
2020-11-13 12:23:04 +01:00
secondaryActions . append ( action )
}
if ! secondaryActions . isEmpty {
menuElements . append ( UIMenu ( title : " " , options : . displayInline , children : secondaryActions ) )
2020-01-08 00:36:32 +01:00
}
if let action = self . openInBrowserAction ( article ) {
2020-11-13 12:23:04 +01:00
menuElements . append ( UIMenu ( title : " " , options : . displayInline , children : [ action ] ) )
2020-01-08 00:36:32 +01:00
}
if let action = self . shareAction ( article , indexPath : indexPath ) {
2020-11-13 12:23:04 +01:00
menuElements . append ( UIMenu ( title : " " , options : . displayInline , children : [ action ] ) )
2020-01-08 00:36:32 +01:00
}
2020-11-13 12:23:04 +01:00
return UIMenu ( title : " " , children : menuElements )
2020-01-08 00:36:32 +01:00
} )
2019-08-16 20:19:06 +02:00
}
2019-11-24 05:15:29 +01:00
override func tableView ( _ tableView : UITableView , previewForHighlightingContextMenuWithConfiguration configuration : UIContextMenuConfiguration ) -> UITargetedPreview ? {
guard let row = configuration . identifier as ? Int ,
let cell = tableView . cellForRow ( at : IndexPath ( row : row , section : 0 ) ) else {
return nil
}
return UITargetedPreview ( view : cell , parameters : CroppingPreviewParameters ( view : cell ) )
}
2019-04-15 22:03:05 +02:00
override func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
2019-09-04 23:24:16 +02:00
becomeFirstResponder ( )
2019-09-11 16:11:33 +02:00
let article = dataSource . itemIdentifier ( for : indexPath )
2020-01-28 05:57:52 +01:00
coordinator . selectArticle ( article , animations : [ . scroll , . select , . navigation ] )
2019-04-15 22:03:05 +02:00
}
2019-11-11 23:59:42 +01:00
override func scrollViewDidScroll ( _ scrollView : UIScrollView ) {
scrollPositionQueue . add ( self , #selector ( scrollPositionDidChange ) )
}
2019-04-15 22:03:05 +02:00
// MARK: N o t i f i c a t i o n s
2019-04-23 11:35:48 +02:00
@objc dynamic func unreadCountDidChange ( _ notification : Notification ) {
updateUI ( )
}
2019-04-15 22:03:05 +02:00
@objc func statusesDidChange ( _ note : Notification ) {
2019-12-17 07:45:59 +01:00
guard let articleIDs = note . userInfo ? [ Account . UserInfoKey . articleIDs ] as ? Set < String > , ! articleIDs . isEmpty else {
2019-04-15 22:03:05 +02:00
return
}
2019-12-17 07:45:59 +01:00
2019-09-11 03:32:03 +02:00
let visibleArticles = tableView . indexPathsForVisibleRows ! . compactMap { return dataSource . itemIdentifier ( for : $0 ) }
2019-12-17 07:45:59 +01:00
let visibleUpdatedArticles = visibleArticles . filter { articleIDs . contains ( $0 . articleID ) }
2019-09-06 20:45:45 +02:00
for article in visibleUpdatedArticles {
2019-09-11 16:11:33 +02:00
if let indexPath = dataSource . indexPath ( for : article ) {
if let cell = tableView . cellForRow ( at : indexPath ) as ? MasterTimelineTableViewCell {
2019-09-06 20:45:45 +02:00
configure ( cell , article : article )
}
}
}
2019-04-15 22:03:05 +02:00
}
2019-11-15 03:11:41 +01:00
@objc func webFeedIconDidBecomeAvailable ( _ note : Notification ) {
2019-11-24 17:47:09 +01:00
if let titleView = navigationItem . titleView as ? MasterTimelineTitleView {
titleView . iconView . iconImage = coordinator . timelineIconImage
}
2019-11-15 03:11:41 +01:00
guard let feed = note . userInfo ? [ UserInfoKey . webFeed ] as ? WebFeed else {
2019-04-15 22:03:05 +02:00
return
}
2019-08-30 21:17:05 +02:00
tableView . indexPathsForVisibleRows ? . forEach { indexPath in
2019-09-11 03:32:03 +02:00
guard let article = dataSource . itemIdentifier ( for : indexPath ) else {
2019-08-30 21:17:05 +02:00
return
}
2019-11-15 03:11:41 +01:00
if article . webFeed = = feed , let cell = tableView . cellForRow ( at : indexPath ) as ? MasterTimelineTableViewCell , let image = iconImageFor ( article ) {
2019-11-06 01:05:57 +01:00
cell . setIconImage ( image )
2019-04-15 22:03:05 +02:00
}
}
}
@objc func avatarDidBecomeAvailable ( _ note : Notification ) {
2019-11-06 01:05:57 +01:00
guard coordinator . showIcons , let avatarURL = note . userInfo ? [ UserInfoKey . url ] as ? String else {
2019-04-15 22:03:05 +02:00
return
}
2019-08-30 21:17:05 +02:00
tableView . indexPathsForVisibleRows ? . forEach { indexPath in
2019-09-11 03:32:03 +02:00
guard let article = dataSource . itemIdentifier ( for : indexPath ) , let authors = article . authors , ! authors . isEmpty else {
2019-08-30 21:17:05 +02:00
return
}
for author in authors {
2019-11-06 01:05:57 +01:00
if author . avatarURL = = avatarURL , let cell = tableView . cellForRow ( at : indexPath ) as ? MasterTimelineTableViewCell , let image = iconImageFor ( article ) {
cell . setIconImage ( image )
2019-04-15 22:03:05 +02:00
}
}
}
}
2019-08-22 02:37:19 +02:00
@objc func faviconDidBecomeAvailable ( _ note : Notification ) {
2019-11-24 17:47:09 +01:00
if let titleView = navigationItem . titleView as ? MasterTimelineTitleView {
titleView . iconView . iconImage = coordinator . timelineIconImage
}
2019-11-06 01:05:57 +01:00
if coordinator . showIcons {
2019-09-07 00:22:12 +02:00
queueReloadAvailableCells ( )
2019-04-15 22:03:05 +02:00
}
}
2019-04-29 22:29:00 +02:00
@objc func userDefaultsDidChange ( _ note : Notification ) {
2020-07-02 04:47:45 +02:00
if numberOfTextLines != AppDefaults . shared . timelineNumberOfLines || iconSize != AppDefaults . shared . timelineIconSize {
numberOfTextLines = AppDefaults . shared . timelineNumberOfLines
iconSize = AppDefaults . shared . timelineIconSize
2020-03-18 21:55:33 +01:00
resetEstimatedRowHeight ( )
2019-08-30 21:17:05 +02:00
reloadAllVisibleCells ( )
2019-04-29 22:29:00 +02:00
}
2021-02-04 01:19:35 +01:00
updateToolbar ( )
2019-04-29 22:29:00 +02:00
}
2019-04-29 21:50:56 +02:00
@objc func contentSizeCategoryDidChange ( _ note : Notification ) {
2019-08-30 21:17:05 +02:00
reloadAllVisibleCells ( )
2019-04-29 21:50:56 +02:00
}
2019-09-28 02:45:09 +02:00
@objc func displayNameDidChange ( _ note : Notification ) {
2019-11-24 17:47:09 +01:00
if let titleView = navigationItem . titleView as ? MasterTimelineTitleView {
titleView . label . text = coordinator . timelineFeed ? . nameForDisplay
}
2019-09-28 02:45:09 +02:00
}
2020-01-07 05:23:39 +01:00
@objc func willEnterForeground ( _ note : Notification ) {
updateUI ( )
}
2019-11-11 23:59:42 +01:00
@objc func scrollPositionDidChange ( ) {
coordinator . timelineMiddleIndexPath = tableView . middleVisibleRow ( )
}
2019-04-15 22:03:05 +02:00
// MARK: R e l o a d i n g
2019-09-07 00:22:12 +02:00
func queueReloadAvailableCells ( ) {
CoalescingQueue . standard . add ( self , #selector ( reloadAllVisibleCells ) )
}
@objc private func reloadAllVisibleCells ( ) {
2019-09-11 03:32:03 +02:00
let visibleArticles = tableView . indexPathsForVisibleRows ! . compactMap { return dataSource . itemIdentifier ( for : $0 ) }
2019-08-30 21:17:05 +02:00
reloadCells ( visibleArticles )
2019-04-15 22:03:05 +02:00
}
2019-08-30 21:17:05 +02:00
private func reloadCells ( _ articles : [ Article ] ) {
var snapshot = dataSource . snapshot ( )
snapshot . reloadItems ( articles )
dataSource . apply ( snapshot , animatingDifferences : false ) { [ weak self ] in
2019-09-29 22:53:50 +02:00
self ? . restoreSelectionIfNecessary ( adjustScroll : false )
2019-08-30 21:17:05 +02:00
}
2019-04-15 22:03:05 +02:00
}
2019-04-29 23:29:53 +02:00
// MARK: C e l l C o n f i g u r i n g
2020-03-18 21:55:33 +01:00
private func resetEstimatedRowHeight ( ) {
2019-04-29 23:29:53 +02:00
2020-03-18 21:55:33 +01:00
let longTitle = " But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure? "
let prototypeID = " prototype "
2020-04-13 02:12:36 +02:00
let status = ArticleStatus ( articleID : prototypeID , read : false , starred : false , dateArrived : Date ( ) )
2020-03-18 21:55:33 +01:00
let prototypeArticle = Article ( accountID : prototypeID , articleID : prototypeID , webFeedID : prototypeID , uniqueID : prototypeID , title : longTitle , contentHTML : nil , contentText : nil , url : nil , externalURL : nil , summary : nil , imageURL : nil , datePublished : nil , dateModified : nil , authors : nil , status : status )
2019-04-29 23:29:53 +02:00
2020-04-18 14:53:56 +02:00
let prototypeCellData = MasterTimelineCellData ( article : prototypeArticle , showFeedName : . feed , feedName : " Prototype Feed Name " , byline : nil , iconImage : nil , showIcon : false , featuredImage : nil , numberOfLines : numberOfTextLines , iconSize : iconSize )
2019-04-29 23:29:53 +02:00
2019-04-30 00:19:08 +02:00
if UIApplication . shared . preferredContentSizeCategory . isAccessibilityCategory {
let layout = MasterTimelineAccessibilityCellLayout ( width : tableView . bounds . width , insets : tableView . safeAreaInsets , cellData : prototypeCellData )
2020-03-18 21:55:33 +01:00
tableView . estimatedRowHeight = layout . height
2019-04-30 00:19:08 +02:00
} else {
let layout = MasterTimelineDefaultCellLayout ( width : tableView . bounds . width , insets : tableView . safeAreaInsets , cellData : prototypeCellData )
2020-03-18 21:55:33 +01:00
tableView . estimatedRowHeight = layout . height
2019-04-30 00:19:08 +02:00
}
2020-03-18 21:55:33 +01:00
2019-04-29 23:29:53 +02:00
}
2020-03-18 21:55:33 +01:00
2019-04-15 22:03:05 +02:00
}
2019-08-31 22:53:47 +02:00
// MARK: S e a r c h i n g
2019-08-31 18:50:34 +02:00
2019-09-02 19:40:14 +02:00
extension MasterTimelineViewController : UISearchControllerDelegate {
func willPresentSearchController ( _ searchController : UISearchController ) {
coordinator . beginSearching ( )
searchController . searchBar . showsScopeBar = true
}
func willDismissSearchController ( _ searchController : UISearchController ) {
coordinator . endSearching ( )
searchController . searchBar . showsScopeBar = false
}
}
2019-08-31 18:50:34 +02:00
extension MasterTimelineViewController : UISearchResultsUpdating {
2019-08-31 22:53:47 +02:00
2019-08-31 18:50:34 +02:00
func updateSearchResults ( for searchController : UISearchController ) {
2019-08-31 22:53:47 +02:00
let searchScope = SearchScope ( rawValue : searchController . searchBar . selectedScopeButtonIndex ) !
coordinator . searchArticles ( searchController . searchBar . text ! , searchScope )
2019-08-31 18:50:34 +02:00
}
2019-08-31 22:53:47 +02:00
}
extension MasterTimelineViewController : UISearchBarDelegate {
func searchBar ( _ searchBar : UISearchBar , selectedScopeButtonIndexDidChange selectedScope : Int ) {
let searchScope = SearchScope ( rawValue : selectedScope ) !
coordinator . searchArticles ( searchBar . text ! , searchScope )
}
2019-08-31 18:50:34 +02:00
}
2019-04-15 22:03:05 +02:00
// MARK: P r i v a t e
private extension MasterTimelineViewController {
2019-04-22 23:25:16 +02:00
2020-01-03 22:23:37 +01:00
func configureToolbar ( ) {
2020-03-13 22:03:42 +01:00
guard ! coordinator . isThreePanelMode else {
2020-01-03 22:23:37 +01:00
return
}
guard let refreshProgressView = Bundle . main . loadNibNamed ( " RefreshProgressView " , owner : self , options : nil ) ? [ 0 ] as ? RefreshProgressView else {
return
}
self . refreshProgressView = refreshProgressView
let refreshProgressItemButton = UIBarButtonItem ( customView : refreshProgressView )
toolbarItems ? . insert ( refreshProgressItemButton , at : 2 )
}
2019-12-09 02:14:33 +01:00
func resetUI ( resetScroll : Bool ) {
2019-09-28 02:45:09 +02:00
2019-11-24 20:37:56 +01:00
title = coordinator . timelineFeed ? . nameForDisplay ? ? " Timeline "
2019-11-24 17:47:09 +01:00
if let titleView = navigationItem . titleView as ? MasterTimelineTitleView {
2020-12-06 23:01:43 +01:00
let timelineIconImage = coordinator . timelineIconImage
titleView . iconView . iconImage = timelineIconImage
if let preferredColor = timelineIconImage ? . preferredColor {
titleView . iconView . tintColor = UIColor ( cgColor : preferredColor )
} else {
titleView . iconView . tintColor = nil
}
2019-11-15 13:19:14 +01:00
titleView . label . text = coordinator . timelineFeed ? . nameForDisplay
2019-10-25 22:03:13 +02:00
updateTitleUnreadCount ( )
2019-10-01 03:01:02 +02:00
2019-11-15 13:19:14 +01:00
if coordinator . timelineFeed is WebFeed {
2020-03-24 22:42:46 +01:00
titleView . buttonize ( )
2019-11-24 17:47:09 +01:00
titleView . addGestureRecognizer ( feedTapGestureRecognizer )
} else {
2020-03-24 22:42:46 +01:00
titleView . debuttonize ( )
2019-11-24 17:47:09 +01:00
titleView . removeGestureRecognizer ( feedTapGestureRecognizer )
2019-09-28 02:45:09 +02:00
}
2019-09-22 00:59:58 +02:00
navigationItem . titleView = titleView
}
2019-11-27 18:43:36 +01:00
switch coordinator . timelineDefaultReadFilterType {
case . none , . read :
2020-08-07 22:20:20 +02:00
navigationItem . rightBarButtonItem = filterButton
2019-11-24 10:47:29 +01:00
case . alwaysRead :
2020-08-07 22:20:20 +02:00
navigationItem . rightBarButtonItem = nil
2019-11-22 01:22:43 +01:00
}
2019-04-22 23:25:16 +02:00
2019-11-27 18:43:36 +01:00
if coordinator . isReadArticlesFiltered {
2020-03-22 16:18:07 +01:00
filterButton ? . image = AppAssets . filterActiveImage
filterButton ? . accLabelText = NSLocalizedString ( " Selected - Filter Read Articles " , comment : " Selected - Filter Read Articles " )
2019-11-27 18:43:36 +01:00
} else {
2020-03-22 16:18:07 +01:00
filterButton ? . image = AppAssets . filterInactiveImage
filterButton ? . accLabelText = NSLocalizedString ( " Filter Read Articles " , comment : " Filter Read Articles " )
2019-11-27 18:43:36 +01:00
}
2020-03-22 16:18:07 +01:00
2019-07-27 21:36:01 +02:00
tableView . selectRow ( at : nil , animated : false , scrollPosition : . top )
2020-10-19 02:59:11 +02:00
2020-10-18 03:06:53 +02:00
if resetScroll {
let snapshot = dataSource . snapshot ( )
if snapshot . sectionIdentifiers . count > 0 && snapshot . itemIdentifiers ( inSection : 0 ) . count > 0 {
tableView . scrollToRow ( at : IndexPath ( row : 0 , section : 0 ) , at : . top , animated : false )
}
2019-04-22 23:25:16 +02:00
}
2019-10-08 16:19:50 +02:00
updateToolbar ( )
2019-04-23 01:00:26 +02:00
}
2019-10-08 16:19:50 +02:00
func updateToolbar ( ) {
2020-08-10 03:13:37 +02:00
guard firstUnreadButton != nil else { return }
2019-06-29 20:35:12 +02:00
markAllAsReadButton . isEnabled = coordinator . isTimelineUnreadAvailable
firstUnreadButton . isEnabled = coordinator . isTimelineUnreadAvailable
2020-08-10 03:13:37 +02:00
2020-03-13 22:03:42 +01:00
if coordinator . isRootSplitCollapsed {
2020-08-07 22:20:20 +02:00
if let toolbarItems = toolbarItems , toolbarItems . last != firstUnreadButton {
var items = toolbarItems
items . append ( firstUnreadButton )
setToolbarItems ( items , animated : false )
}
2020-03-13 22:03:42 +01:00
} else {
2020-08-07 22:20:20 +02:00
if let toolbarItems = toolbarItems , toolbarItems . last = = firstUnreadButton {
let items = Array ( toolbarItems [ 0. . < toolbarItems . count - 1 ] )
setToolbarItems ( items , animated : false )
}
2020-03-13 22:03:42 +01:00
}
2021-02-01 01:12:20 +01:00
if #available ( iOS 14 , * ) {
let title = NSLocalizedString ( " Mark All as Read " , comment : " Mark All as Read " )
var markAsReadAction : UIAction !
if AppDefaults . shared . confirmMarkAllAsRead {
markAsReadAction = UIAction ( title : title , image : AppAssets . markAllAsReadImage , discoverabilityTitle : " in \( self . title ! ) " ) { [ weak self ] action in
self ? . coordinator . markAllAsReadInTimeline ( )
}
let settingsAction = UIAction ( title : NSLocalizedString ( " Settings " , comment : " Settings " ) , image : UIImage ( systemName : " gear " ) ! , discoverabilityTitle : NSLocalizedString ( " You can turn this confirmation off in Settings. " , comment : " You can turn this confirmation off in Settings. " ) ) { [ weak self ] action in
self ? . coordinator . showSettings ( scrollToArticlesSection : true )
}
markAllAsReadButton . menu = UIMenu ( title : NSLocalizedString ( title , comment : title ) , image : nil , identifier : nil , children : [ settingsAction , markAsReadAction ] )
2021-02-04 01:19:35 +01:00
markAllAsReadButton . action = nil
2021-02-01 01:12:20 +01:00
} else {
markAllAsReadButton . action = #selector ( MasterTimelineViewController . markAllAsRead ( _ : ) )
}
}
2019-04-22 23:25:16 +02:00
}
2019-04-18 21:36:22 +02:00
2019-10-25 22:03:13 +02:00
func updateTitleUnreadCount ( ) {
2019-11-24 17:47:09 +01:00
if let titleView = navigationItem . titleView as ? MasterTimelineTitleView {
titleView . unreadCountView . unreadCount = coordinator . unreadCount
}
2019-10-01 03:01:02 +02:00
}
2019-11-19 18:16:43 +01:00
func applyChanges ( animated : Bool , completion : ( ( ) -> Void ) ? = nil ) {
2020-03-18 22:06:05 +01:00
if coordinator . articles . count = = 0 {
tableView . rowHeight = tableView . estimatedRowHeight
} else {
tableView . rowHeight = UITableView . automaticDimension
}
2019-08-30 21:17:05 +02:00
var snapshot = NSDiffableDataSourceSnapshot < Int , Article > ( )
snapshot . appendSections ( [ 0 ] )
snapshot . appendItems ( coordinator . articles , toSection : 0 )
2020-03-18 22:06:05 +01:00
2019-11-19 18:16:43 +01:00
dataSource . apply ( snapshot , animatingDifferences : animated ) { [ weak self ] in
2019-09-29 22:53:50 +02:00
self ? . restoreSelectionIfNecessary ( adjustScroll : false )
2019-08-30 21:17:05 +02:00
completion ? ( )
}
}
func makeDataSource ( ) -> UITableViewDiffableDataSource < Int , Article > {
2019-11-19 18:16:43 +01:00
let dataSource : UITableViewDiffableDataSource < Int , Article > =
2019-11-22 22:23:21 +01:00
MasterTimelineDataSource ( tableView : tableView , cellProvider : { [ weak self ] tableView , indexPath , article in
2019-11-19 18:16:43 +01:00
let cell = tableView . dequeueReusableCell ( withIdentifier : " Cell " , for : indexPath ) as ! MasterTimelineTableViewCell
self ? . configure ( cell , article : article )
return cell
} )
2019-11-22 02:54:35 +01:00
dataSource . defaultRowAnimation = . middle
2019-11-19 18:16:43 +01:00
return dataSource
2019-08-30 21:17:05 +02:00
}
2019-07-27 21:49:07 +02:00
2019-08-30 21:17:05 +02:00
func configure ( _ cell : MasterTimelineTableViewCell , article : Article ) {
2019-04-15 22:03:05 +02:00
2019-11-06 01:05:57 +01:00
let iconImage = iconImageFor ( article )
2019-04-15 22:03:05 +02:00
let featuredImage = featuredImageFor ( article )
2019-06-29 20:35:12 +02:00
let showFeedNames = coordinator . showFeedNames
2019-11-06 01:05:57 +01:00
let showIcon = coordinator . showIcons && iconImage != nil
2020-04-18 14:53:56 +02:00
cell . cellData = MasterTimelineCellData ( article : article , showFeedName : showFeedNames , feedName : article . webFeed ? . nameForDisplay , byline : article . byline ( ) , iconImage : iconImage , showIcon : showIcon , featuredImage : featuredImage , numberOfLines : numberOfTextLines , iconSize : iconSize )
2019-04-15 22:03:05 +02:00
}
2019-11-06 01:05:57 +01:00
func iconImageFor ( _ article : Article ) -> IconImage ? {
if ! coordinator . showIcons {
2019-04-15 22:03:05 +02:00
return nil
}
2019-11-06 01:05:57 +01:00
return article . iconImage ( )
2019-04-15 22:03:05 +02:00
}
func featuredImageFor ( _ article : Article ) -> UIImage ? {
if let url = article . imageURL , let data = appDelegate . imageDownloader . image ( for : url ) {
return RSImage ( data : data )
}
return nil
}
2020-02-18 22:49:29 +01:00
func toggleArticleReadStatusAction ( _ article : Article ) -> UIAction ? {
guard ! article . status . read || article . isAvailableToMarkUnread else { return nil }
2019-08-16 20:19:06 +02:00
let title = article . status . read ?
NSLocalizedString ( " Mark as Unread " , comment : " Mark as Unread " ) :
NSLocalizedString ( " Mark as Read " , comment : " Mark as Read " )
let image = article . status . read ? AppAssets . circleClosedImage : AppAssets . circleOpenImage
2019-08-19 00:34:53 +02:00
let action = UIAction ( title : title , image : image ) { [ weak self ] action in
2019-09-11 16:11:33 +02:00
self ? . coordinator . toggleRead ( article )
2019-08-16 20:19:06 +02:00
}
return action
}
2019-09-11 16:11:33 +02:00
func toggleArticleStarStatusAction ( _ article : Article ) -> UIAction {
2019-08-16 20:19:06 +02:00
let title = article . status . starred ?
NSLocalizedString ( " Mark as Unstarred " , comment : " Mark as Unstarred " ) :
NSLocalizedString ( " Mark as Starred " , comment : " Mark as Starred " )
let image = article . status . starred ? AppAssets . starOpenImage : AppAssets . starClosedImage
2019-08-19 00:34:53 +02:00
let action = UIAction ( title : title , image : image ) { [ weak self ] action in
2019-09-11 16:11:33 +02:00
self ? . coordinator . toggleStar ( article )
2019-08-16 20:19:06 +02:00
}
return action
}
2020-01-03 08:16:55 +01:00
2020-05-13 06:33:51 +02:00
func markAboveAsReadAction ( _ article : Article , indexPath : IndexPath ) -> UIAction ? {
guard coordinator . canMarkAboveAsRead ( for : article ) , let contentView = self . tableView . cellForRow ( at : indexPath ) ? . contentView else {
2020-01-03 08:16:55 +01:00
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
2020-05-13 06:33:51 +02:00
MarkAsReadAlertController . confirm ( self , coordinator : self ? . coordinator , confirmTitle : title , sourceType : contentView ) { [ weak self ] in
2020-01-11 19:30:16 +01:00
self ? . coordinator . markAboveAsRead ( article )
}
2020-01-03 08:16:55 +01:00
}
return action
}
2019-08-16 20:19:06 +02:00
2020-05-13 06:33:51 +02:00
func markBelowAsReadAction ( _ article : Article , indexPath : IndexPath ) -> UIAction ? {
guard coordinator . canMarkBelowAsRead ( for : article ) , let contentView = self . tableView . cellForRow ( at : indexPath ) ? . contentView else {
2020-01-03 08:16:55 +01:00
return nil
}
let title = NSLocalizedString ( " Mark Below as Read " , comment : " Mark Below as Read " )
let image = AppAssets . markBelowAsReadImage
2019-08-19 00:34:53 +02:00
let action = UIAction ( title : title , image : image ) { [ weak self ] action in
2020-05-13 06:33:51 +02:00
MarkAsReadAlertController . confirm ( self , coordinator : self ? . coordinator , confirmTitle : title , sourceType : contentView ) { [ weak self ] in
2020-01-11 19:30:16 +01:00
self ? . coordinator . markBelowAsRead ( article )
}
2019-08-19 00:34:53 +02:00
}
return action
}
2020-05-13 06:33:51 +02:00
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 {
2020-01-03 08:16:55 +01:00
return nil
}
let title = NSLocalizedString ( " Mark Above as Read " , comment : " Mark Above as Read " )
2020-01-11 19:30:16 +01:00
let cancel = {
2020-01-03 08:16:55 +01:00
completion ( true )
}
2020-01-11 19:30:16 +01:00
let action = UIAlertAction ( title : title , style : . default ) { [ weak self ] action in
2020-05-13 06:33:51 +02:00
MarkAsReadAlertController . confirm ( self , coordinator : self ? . coordinator , confirmTitle : title , sourceType : contentView , cancelCompletion : cancel ) { [ weak self ] in
2020-01-11 19:30:16 +01:00
self ? . coordinator . markAboveAsRead ( article )
completion ( true )
}
}
2020-01-03 08:16:55 +01:00
return action
}
2020-05-13 06:33:51 +02:00
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 {
2020-01-03 08:16:55 +01:00
return nil
}
let title = NSLocalizedString ( " Mark Below as Read " , comment : " Mark Below as Read " )
2020-01-11 19:30:16 +01:00
let cancel = {
2019-12-15 01:14:55 +01:00
completion ( true )
2019-08-19 00:34:53 +02:00
}
2020-01-11 19:30:16 +01:00
let action = UIAlertAction ( title : title , style : . default ) { [ weak self ] action in
2020-05-13 06:33:51 +02:00
MarkAsReadAlertController . confirm ( self , coordinator : self ? . coordinator , confirmTitle : title , sourceType : contentView , cancelCompletion : cancel ) { [ weak self ] in
2020-01-11 19:30:16 +01:00
self ? . coordinator . markBelowAsRead ( article )
completion ( true )
}
}
2019-08-19 00:34:53 +02:00
return action
}
2019-09-11 16:11:33 +02:00
func discloseFeedAction ( _ article : Article ) -> UIAction ? {
2020-02-02 21:44:54 +01:00
guard let webFeed = article . webFeed ,
! coordinator . timelineFeedIsEqualTo ( webFeed ) else { return nil }
2019-09-11 16:11:33 +02:00
2019-11-13 22:22:22 +01:00
let title = NSLocalizedString ( " Go to Feed " , comment : " Go to Feed " )
2019-08-19 22:45:52 +02:00
let action = UIAction ( title : title , image : AppAssets . openInSidebarImage ) { [ weak self ] action in
2020-02-18 02:40:40 +01:00
self ? . coordinator . discloseWebFeed ( webFeed , animations : [ . scroll , . navigation ] )
2019-08-19 22:45:52 +02:00
}
return action
}
2019-12-15 01:14:55 +01:00
func discloseFeedAlertAction ( _ article : Article , completion : @ escaping ( Bool ) -> Void ) -> UIAlertAction ? {
2020-02-02 21:44:54 +01:00
guard let webFeed = article . webFeed ,
! coordinator . timelineFeedIsEqualTo ( webFeed ) else { return nil }
2019-09-11 16:11:33 +02:00
2019-11-13 22:22:22 +01:00
let title = NSLocalizedString ( " Go to Feed " , comment : " Go to Feed " )
2019-08-19 22:45:52 +02:00
let action = UIAlertAction ( title : title , style : . default ) { [ weak self ] action in
2020-02-18 02:40:40 +01:00
self ? . coordinator . discloseWebFeed ( webFeed , animations : [ . scroll , . navigation ] )
2019-12-15 01:14:55 +01:00
completion ( true )
2019-08-19 22:45:52 +02:00
}
return action
}
2020-05-13 06:33:51 +02:00
func markAllInFeedAsReadAction ( _ article : Article , indexPath : IndexPath ) -> UIAction ? {
2019-11-15 03:11:41 +01:00
guard let webFeed = article . webFeed else { return nil }
2019-12-17 07:45:59 +01:00
guard let fetchedArticles = try ? webFeed . fetchArticles ( ) else {
return nil
}
2019-09-11 16:11:33 +02:00
2019-12-17 07:45:59 +01:00
let articles = Array ( fetchedArticles )
2020-05-13 06:33:51 +02:00
guard articles . canMarkAllAsRead ( ) , let contentView = self . tableView . cellForRow ( at : indexPath ) ? . contentView else {
2019-08-20 00:26:09 +02:00
return nil
}
2020-05-13 06:33:51 +02:00
2019-08-20 00:26:09 +02:00
let localizedMenuText = NSLocalizedString ( " Mark All as Read in “%@” " , comment : " Command " )
2019-11-15 03:11:41 +01:00
let title = NSString . localizedStringWithFormat ( localizedMenuText as NSString , webFeed . nameForDisplay ) as String
2019-08-20 00:26:09 +02:00
2020-01-07 02:07:04 +01:00
let action = UIAction ( title : title , image : AppAssets . markAllAsReadImage ) { [ weak self ] action in
2020-05-13 06:33:51 +02:00
MarkAsReadAlertController . confirm ( self , coordinator : self ? . coordinator , confirmTitle : title , sourceType : contentView ) { [ weak self ] in
2020-01-11 19:30:16 +01:00
self ? . coordinator . markAllAsRead ( articles )
}
2019-08-20 00:26:09 +02:00
}
return action
}
2020-05-13 06:33:51 +02:00
func markAllInFeedAsReadAlertAction ( _ article : Article , indexPath : IndexPath , completion : @ escaping ( Bool ) -> Void ) -> UIAlertAction ? {
2019-11-15 03:11:41 +01:00
guard let webFeed = article . webFeed else { return nil }
2019-12-17 07:45:59 +01:00
guard let fetchedArticles = try ? webFeed . fetchArticles ( ) else {
return nil
}
let articles = Array ( fetchedArticles )
2020-05-13 06:33:51 +02:00
guard articles . canMarkAllAsRead ( ) , let contentView = self . tableView . cellForRow ( at : indexPath ) ? . contentView else {
2019-08-20 00:26:09 +02:00
return nil
}
2019-08-20 00:38:30 +02:00
let localizedMenuText = NSLocalizedString ( " Mark All as Read in “%@” " , comment : " Mark All as Read in Feed " )
2019-11-15 03:11:41 +01:00
let title = NSString . localizedStringWithFormat ( localizedMenuText as NSString , webFeed . nameForDisplay ) as String
2020-01-11 19:30:16 +01:00
let cancel = {
completion ( true )
}
2019-08-20 00:26:09 +02:00
let action = UIAlertAction ( title : title , style : . default ) { [ weak self ] action in
2020-05-13 06:33:51 +02:00
MarkAsReadAlertController . confirm ( self , coordinator : self ? . coordinator , confirmTitle : title , sourceType : contentView , cancelCompletion : cancel ) { [ weak self ] in
2020-01-11 19:30:16 +01:00
self ? . coordinator . markAllAsRead ( articles )
completion ( true )
}
2019-08-20 00:26:09 +02:00
}
return action
}
2019-08-20 00:38:30 +02:00
2019-09-11 16:11:33 +02:00
func openInBrowserAction ( _ article : Article ) -> UIAction ? {
guard let preferredLink = article . preferredLink , let _ = URL ( string : preferredLink ) else {
2019-08-20 00:38:30 +02:00
return nil
}
let title = NSLocalizedString ( " Open in Browser " , comment : " Open in Browser " )
let action = UIAction ( title : title , image : AppAssets . safariImage ) { [ weak self ] action in
2019-09-11 16:11:33 +02:00
self ? . coordinator . showBrowserForArticle ( article )
2019-08-20 00:38:30 +02:00
}
return action
}
2019-12-15 01:14:55 +01:00
func openInBrowserAlertAction ( _ article : Article , completion : @ escaping ( Bool ) -> Void ) -> UIAlertAction ? {
2019-09-11 16:11:33 +02:00
guard let preferredLink = article . preferredLink , let _ = URL ( string : preferredLink ) else {
2019-08-20 00:38:30 +02:00
return nil
}
let title = NSLocalizedString ( " Open in Browser " , comment : " Open in Browser " )
let action = UIAlertAction ( title : title , style : . default ) { [ weak self ] action in
2019-09-11 16:11:33 +02:00
self ? . coordinator . showBrowserForArticle ( article )
2019-12-15 01:14:55 +01:00
completion ( true )
2019-08-20 00:38:30 +02:00
}
return action
}
2019-08-20 00:26:09 +02:00
2019-08-20 01:09:38 +02:00
func shareDialogForTableCell ( indexPath : IndexPath , url : URL , title : String ? ) {
2020-01-11 09:12:41 +01:00
let activityViewController = UIActivityViewController ( url : url , title : title , applicationActivities : nil )
2019-08-20 01:09:38 +02:00
guard let cell = tableView . cellForRow ( at : indexPath ) else { return }
let popoverController = activityViewController . popoverPresentationController
popoverController ? . sourceView = cell
popoverController ? . sourceRect = CGRect ( x : 0 , y : 0 , width : cell . frame . size . width , height : cell . frame . size . height )
present ( activityViewController , animated : true )
}
2019-09-11 16:11:33 +02:00
func shareAction ( _ article : Article , indexPath : IndexPath ) -> UIAction ? {
2019-08-20 01:09:38 +02:00
guard let preferredLink = article . preferredLink , let url = URL ( string : preferredLink ) else {
return nil
}
let title = NSLocalizedString ( " Share " , comment : " Share " )
let action = UIAction ( title : title , image : AppAssets . shareImage ) { [ weak self ] action in
self ? . shareDialogForTableCell ( indexPath : indexPath , url : url , title : article . title )
}
return action
}
2019-12-15 01:14:55 +01:00
func shareAlertAction ( _ article : Article , indexPath : IndexPath , completion : @ escaping ( Bool ) -> Void ) -> UIAlertAction ? {
2019-08-20 01:09:38 +02:00
guard let preferredLink = article . preferredLink , let url = URL ( string : preferredLink ) else {
return nil
}
let title = NSLocalizedString ( " Share " , comment : " Share " )
let action = UIAlertAction ( title : title , style : . default ) { [ weak self ] action in
2019-12-15 01:14:55 +01:00
completion ( true )
2019-08-20 01:09:38 +02:00
self ? . shareDialogForTableCell ( indexPath : indexPath , url : url , title : article . title )
}
return action
}
2019-04-15 22:03:05 +02:00
}