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-04-15 22:03:05 +02:00
2019-04-23 11:35:48 +02:00
@IBOutlet weak var markAllAsReadButton : UIBarButtonItem !
@IBOutlet weak var firstUnreadButton : UIBarButtonItem !
2019-04-22 22:31:34 +02:00
2019-06-29 20:35:12 +02:00
weak var coordinator : AppCoordinator !
2019-04-21 20:57:23 +02:00
var undoableCommands = [ UndoableCommand ] ( )
2019-04-17 01:08:02 +02:00
override var canBecomeFirstResponder : Bool {
return true
}
2019-04-15 22:03:05 +02:00
override func viewDidLoad ( ) {
super . viewDidLoad ( )
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 )
NotificationCenter . default . addObserver ( self , selector : #selector ( feedIconDidBecomeAvailable ( _ : ) ) , name : . FeedIconDidBecomeAvailable , object : nil )
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-07-27 21:49:07 +02:00
NotificationCenter . default . addObserver ( self , selector : #selector ( progressDidChange ( _ : ) ) , name : . AccountRefreshProgressDidChange , object : nil )
2019-04-15 22:03:05 +02:00
2019-06-29 20:35:12 +02:00
NotificationCenter . default . addObserver ( self , selector : #selector ( articlesReinitialized ( _ : ) ) , name : . ArticlesReinitialized , object : coordinator )
NotificationCenter . default . addObserver ( self , selector : #selector ( articleDataDidChange ( _ : ) ) , name : . ArticleDataDidChange , object : coordinator )
NotificationCenter . default . addObserver ( self , selector : #selector ( articlesDidChange ( _ : ) ) , name : . ArticlesDidChange , object : coordinator )
NotificationCenter . default . addObserver ( self , selector : #selector ( articleSelectionDidChange ( _ : ) ) , name : . ArticleSelectionDidChange , object : coordinator )
2019-04-29 21:50:56 +02:00
NotificationCenter . default . addObserver ( self , selector : #selector ( contentSizeCategoryDidChange ) , name : UIContentSizeCategory . didChangeNotification , object : nil )
2019-04-21 20:57:23 +02:00
2019-04-18 21:36:22 +02:00
refreshControl = UIRefreshControl ( )
refreshControl ! . addTarget ( self , action : #selector ( refreshAccounts ( _ : ) ) , for : . valueChanged )
2019-04-29 22:29:00 +02:00
numberOfTextLines = AppDefaults . timelineNumberOfLines
2019-04-29 23:29:53 +02:00
resetEstimatedRowHeight ( )
2019-04-29 22:29:00 +02:00
2019-04-23 01:00:26 +02:00
resetUI ( )
2019-04-22 22:31:34 +02:00
2019-04-15 22:03:05 +02:00
}
2019-08-02 17:25:47 +02:00
override func viewWillAppear ( _ animated : Bool ) {
clearsSelectionOnViewWillAppear = coordinator . isRootSplitCollapsed
super . viewWillAppear ( animated )
}
2019-04-17 01:08:02 +02:00
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
becomeFirstResponder ( )
2019-07-27 21:49:07 +02:00
updateProgressIndicatorIfNeeded ( )
2019-04-17 01:08:02 +02:00
}
override func viewWillDisappear ( _ animated : Bool ) {
super . viewWillDisappear ( animated )
resignFirstResponder ( )
}
2019-06-20 18:58:26 +02:00
override func traitCollectionDidChange ( _ previousTraitCollection : UITraitCollection ? ) {
super . traitCollectionDidChange ( previousTraitCollection )
if traitCollection . userInterfaceStyle != previousTraitCollection ? . userInterfaceStyle {
appDelegate . authorAvatarDownloader . resetCache ( )
appDelegate . feedIconDownloader . resetCache ( )
appDelegate . faviconDownloader . resetCache ( )
performBlockAndRestoreSelection {
tableView . reloadData ( )
}
}
}
2019-04-15 22:03:05 +02:00
// M A R K A c t i o n s
@IBAction func markAllAsRead ( _ sender : Any ) {
let title = NSLocalizedString ( " Mark All Read " , comment : " Mark All Read " )
let message = NSLocalizedString ( " Mark all articles in this timeline as read? " , comment : " Mark all articles " )
let alertController = UIAlertController ( title : title , message : message , preferredStyle : . alert )
let cancelTitle = NSLocalizedString ( " Cancel " , comment : " Cancel " )
let cancelAction = UIAlertAction ( title : cancelTitle , style : . cancel )
alertController . addAction ( cancelAction )
let markTitle = NSLocalizedString ( " Mark All Read " , comment : " Mark All Read " )
let markAction = UIAlertAction ( title : markTitle , style : . default ) { [ weak self ] ( action ) in
2019-07-06 19:31:07 +02:00
self ? . coordinator . markAllAsReadInTimeline ( )
2019-04-15 22:03:05 +02:00
}
2019-06-26 00:39:07 +02:00
2019-04-15 22:03:05 +02:00
alertController . addAction ( markAction )
present ( alertController , animated : true )
}
2019-04-23 11:35:48 +02:00
@IBAction func firstUnread ( _ sender : Any ) {
2019-08-04 00:07:43 +02:00
coordinator . selectNextUnread ( )
2019-04-23 01:00:26 +02:00
}
2019-04-15 22:03:05 +02:00
// MARK: - T a b l e v i e w
override func numberOfSections ( in tableView : UITableView ) -> Int {
return 1
}
override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int {
2019-06-29 20:35:12 +02:00
return coordinator . articles . count
2019-04-15 22:03:05 +02:00
}
override func tableView ( _ tableView : UITableView , trailingSwipeActionsConfigurationForRowAt indexPath : IndexPath ) -> UISwipeActionsConfiguration ? {
2019-06-29 20:35:12 +02:00
let article = coordinator . articles [ indexPath . row ]
2019-04-15 22:03:05 +02:00
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-04-29 13:01:53 +02:00
let readAction = UIContextualAction ( style : . normal , title : readTitle ) { [ weak self ] ( action , view , completionHandler ) in
2019-08-19 23:03:07 +02:00
self ? . coordinator . toggleRead ( for : indexPath )
2019-04-15 22:03:05 +02:00
completionHandler ( true )
}
2019-04-29 13:01:53 +02:00
readAction . image = AppAssets . circleClosedImage
2019-06-19 01:31:37 +02:00
readAction . backgroundColor = AppAssets . netNewsWireBlueColor
2019-04-15 22:03:05 +02:00
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-04-29 13:01:53 +02:00
let starAction = UIContextualAction ( style : . normal , title : starTitle ) { [ weak self ] ( action , view , completionHandler ) in
2019-08-19 23:03:07 +02:00
self ? . coordinator . toggleStar ( for : indexPath )
2019-04-15 22:03:05 +02:00
completionHandler ( true )
}
2019-04-29 13:01:53 +02:00
starAction . image = AppAssets . starClosedImage
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 " )
let moreAction = UIContextualAction ( style : . normal , title : moreTitle ) { [ weak self ] ( action , view , completionHandler ) in
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 )
}
alert . addAction ( self . markOlderAsReadAlertAction ( indexPath : indexPath , completionHandler : completionHandler ) )
2019-08-19 22:45:52 +02:00
if let action = self . discloseFeedAlertAction ( indexPath : indexPath , completionHandler : completionHandler ) {
alert . addAction ( action )
}
2019-08-20 00:26:09 +02:00
if let action = self . markAllInFeedAsReadAlertAction ( indexPath : indexPath , completionHandler : completionHandler ) {
alert . addAction ( action )
}
2019-08-20 00:38:30 +02:00
if let action = self . openInBrowserAlertAction ( indexPath : indexPath , completionHandler : completionHandler ) {
alert . addAction ( action )
}
2019-08-20 01:09:38 +02:00
if let action = self . shareAlertAction ( indexPath : indexPath , completionHandler : completionHandler ) {
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
completionHandler ( true )
} )
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
let configuration = UISwipeActionsConfiguration ( actions : [ readAction , starAction , moreAction ] )
2019-04-15 22:03:05 +02:00
return configuration
}
2019-08-16 20:19:06 +02:00
override func tableView ( _ tableView : UITableView , contextMenuConfigurationForRowAt indexPath : IndexPath , point : CGPoint ) -> UIContextMenuConfiguration ? {
2019-08-19 22:45:52 +02:00
return UIContextMenuConfiguration ( identifier : nil , previewProvider : nil , actionProvider : { [ weak self ] suggestedActions in
2019-08-16 20:19:06 +02:00
2019-08-19 22:45:52 +02:00
guard let self = self else { return nil }
2019-08-16 20:19:06 +02:00
var actions = [ UIAction ] ( )
actions . append ( self . toggleArticleReadStatusAction ( indexPath : indexPath ) )
actions . append ( self . toggleArticleStarStatusAction ( indexPath : indexPath ) )
2019-08-19 00:34:53 +02:00
actions . append ( self . markOlderAsReadAction ( indexPath : indexPath ) )
2019-08-16 20:19:06 +02:00
2019-08-19 22:45:52 +02:00
if let action = self . discloseFeedAction ( indexPath : indexPath ) {
actions . append ( action )
}
2019-08-20 00:26:09 +02:00
if let action = self . markAllInFeedAsReadAction ( indexPath : indexPath ) {
actions . append ( action )
}
2019-08-20 00:38:30 +02:00
if let action = self . openInBrowserAction ( indexPath : indexPath ) {
actions . append ( action )
}
2019-08-20 01:09:38 +02:00
if let action = self . shareAction ( indexPath : indexPath ) {
actions . append ( action )
}
2019-08-19 22:49:42 +02:00
return UIMenu ( title : " " , children : actions )
2019-08-16 20:19:06 +02:00
} )
}
2019-04-15 22:03:05 +02:00
override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell {
let cell = tableView . dequeueReusableCell ( withIdentifier : " Cell " , for : indexPath ) as ! MasterTimelineTableViewCell
2019-06-29 20:35:12 +02:00
let article = coordinator . articles [ indexPath . row ]
2019-04-15 22:03:05 +02:00
configureTimelineCell ( cell , article : article )
return cell
}
override func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
2019-07-06 18:32:19 +02:00
coordinator . selectArticle ( indexPath )
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 ) {
guard let articles = note . userInfo ? [ Account . UserInfoKey . articles ] as ? Set < Article > else {
return
}
reloadVisibleCells ( for : articles )
}
@objc func feedIconDidBecomeAvailable ( _ note : Notification ) {
guard let feed = note . userInfo ? [ UserInfoKey . feed ] as ? Feed else {
return
}
performBlockAndRestoreSelection {
tableView . indexPathsForVisibleRows ? . forEach { indexPath in
2019-06-29 20:35:12 +02:00
guard let article = coordinator . articles . articleAtRow ( indexPath . row ) else {
2019-04-15 22:03:05 +02:00
return
}
if feed = = article . feed {
tableView . reloadRows ( at : [ indexPath ] , with : . none )
return
}
}
}
}
@objc func avatarDidBecomeAvailable ( _ note : Notification ) {
2019-06-29 20:35:12 +02:00
guard coordinator . showAvatars , let avatarURL = note . userInfo ? [ UserInfoKey . url ] as ? String else {
2019-04-15 22:03:05 +02:00
return
}
performBlockAndRestoreSelection {
tableView . indexPathsForVisibleRows ? . forEach { indexPath in
2019-06-29 20:35:12 +02:00
guard let article = coordinator . articles . articleAtRow ( indexPath . row ) , let authors = article . authors , ! authors . isEmpty else {
2019-04-15 22:03:05 +02:00
return
}
for author in authors {
if author . avatarURL = = avatarURL {
tableView . reloadRows ( at : [ indexPath ] , with : . none )
}
}
}
}
}
2019-08-22 02:37:19 +02:00
@objc func faviconDidBecomeAvailable ( _ note : Notification ) {
guard coordinator . showAvatars , let faviconURL = note . userInfo ? [ " faviconURL " ] as ? String else {
return
}
performBlockAndRestoreSelection {
tableView . indexPathsForVisibleRows ? . forEach { indexPath in
guard let article = coordinator . articles . articleAtRow ( indexPath . row ) , let articleFaviconURL = article . feed ? . faviconURL else {
return
}
if faviconURL = = articleFaviconURL {
tableView . reloadRows ( at : [ indexPath ] , with : . none )
return
}
}
2019-04-15 22:03:05 +02:00
}
}
2019-04-29 22:29:00 +02:00
@objc func userDefaultsDidChange ( _ note : Notification ) {
if numberOfTextLines != AppDefaults . timelineNumberOfLines {
numberOfTextLines = AppDefaults . timelineNumberOfLines
2019-04-29 23:29:53 +02:00
resetEstimatedRowHeight ( )
2019-04-29 22:29:00 +02:00
tableView . reloadData ( )
}
}
2019-04-21 20:57:23 +02:00
@objc func articlesReinitialized ( _ note : Notification ) {
2019-04-23 01:00:26 +02:00
resetUI ( )
2019-04-15 22:03:05 +02:00
}
2019-04-21 20:57:23 +02:00
@objc func articleDataDidChange ( _ note : Notification ) {
reloadAllVisibleCells ( )
}
@objc func articlesDidChange ( _ note : Notification ) {
2019-04-23 01:03:13 +02:00
performBlockAndRestoreSelection {
tableView . reloadData ( )
}
2019-04-21 20:57:23 +02:00
}
2019-04-22 23:25:16 +02:00
@objc func articleSelectionDidChange ( _ note : Notification ) {
2019-04-22 00:42:26 +02:00
2019-06-29 20:35:12 +02:00
if let indexPath = coordinator . currentArticleIndexPath {
2019-04-22 00:55:28 +02:00
if tableView . indexPathForSelectedRow != indexPath {
tableView . selectRow ( at : indexPath , animated : true , scrollPosition : . middle )
}
2019-04-22 00:42:26 +02:00
}
2019-04-23 11:35:48 +02:00
updateUI ( )
2019-04-23 01:00:26 +02:00
2019-04-22 00:42:26 +02:00
}
2019-04-29 21:50:56 +02:00
@objc func contentSizeCategoryDidChange ( _ note : Notification ) {
tableView . reloadData ( )
}
2019-07-27 21:49:07 +02:00
@objc func progressDidChange ( _ note : Notification ) {
updateProgressIndicatorIfNeeded ( )
}
2019-04-15 22:03:05 +02:00
// MARK: R e l o a d i n g
@objc func reloadAllVisibleCells ( ) {
tableView . beginUpdates ( )
performBlockAndRestoreSelection {
tableView . reloadRows ( at : tableView . indexPathsForVisibleRows ! , with : . none )
}
tableView . endUpdates ( )
}
private func reloadVisibleCells ( for articles : [ Article ] ) {
reloadVisibleCells ( for : Set ( articles . articleIDs ( ) ) )
}
private func reloadVisibleCells ( for articles : Set < Article > ) {
reloadVisibleCells ( for : articles . articleIDs ( ) )
}
private func reloadVisibleCells ( for articleIDs : Set < String > ) {
if articleIDs . isEmpty {
return
}
2019-06-29 20:35:12 +02:00
let indexes = coordinator . indexesForArticleIDs ( articleIDs )
reloadVisibleCells ( for : indexes )
2019-04-15 22:03:05 +02:00
}
private func reloadVisibleCells ( for indexes : IndexSet ) {
performBlockAndRestoreSelection {
tableView . indexPathsForVisibleRows ? . forEach { indexPath in
if indexes . contains ( indexPath . row ) {
tableView . reloadRows ( at : [ indexPath ] , with : . none )
}
}
}
}
2019-04-29 23:29:53 +02:00
// MARK: C e l l C o n f i g u r i n g
private func resetEstimatedRowHeight ( ) {
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 "
let status = ArticleStatus ( articleID : prototypeID , read : false , starred : false , userDeleted : false , dateArrived : Date ( ) )
let prototypeArticle = Article ( accountID : prototypeID , articleID : prototypeID , feedID : prototypeID , uniqueID : prototypeID , title : longTitle , contentHTML : nil , contentText : nil , url : nil , externalURL : nil , summary : nil , imageURL : nil , bannerImageURL : nil , datePublished : nil , dateModified : nil , authors : nil , attachments : nil , status : status )
let prototypeCellData = MasterTimelineCellData ( article : prototypeArticle , showFeedName : true , feedName : " Prototype Feed Name " , avatar : nil , showAvatar : false , featuredImage : nil , numberOfLines : numberOfTextLines )
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 )
tableView . estimatedRowHeight = layout . height
} else {
let layout = MasterTimelineDefaultCellLayout ( width : tableView . bounds . width , insets : tableView . safeAreaInsets , cellData : prototypeCellData )
tableView . estimatedRowHeight = layout . height
}
2019-04-29 23:29:53 +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
2019-04-18 21:36:22 +02:00
@objc private func refreshAccounts ( _ sender : Any ) {
2019-04-23 14:26:35 +02:00
refreshControl ? . endRefreshing ( )
2019-07-18 22:16:54 +02: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 ) {
AccountManager . shared . refreshAll ( errorHandler : ErrorHandler . present ( self ) )
}
2019-04-18 21:36:22 +02:00
}
2019-04-22 23:25:16 +02:00
2019-04-23 01:00:26 +02:00
func resetUI ( ) {
2019-04-22 23:25:16 +02:00
2019-06-29 20:35:12 +02:00
title = coordinator . timelineName
navigationController ? . title = coordinator . timelineName
2019-04-22 23:25:16 +02:00
2019-07-27 21:36:01 +02:00
tableView . selectRow ( at : nil , animated : false , scrollPosition : . top )
2019-06-29 20:35:12 +02:00
if coordinator . articles . count > 0 {
2019-04-22 23:25:16 +02:00
tableView . scrollToRow ( at : IndexPath ( row : 0 , section : 0 ) , at : . top , animated : false )
}
2019-04-23 11:35:48 +02:00
updateUI ( )
2019-04-23 01:00:26 +02:00
}
2019-04-23 11:35:48 +02:00
func updateUI ( ) {
2019-06-29 20:35:12 +02:00
markAllAsReadButton . isEnabled = coordinator . isTimelineUnreadAvailable
firstUnreadButton . isEnabled = coordinator . isTimelineUnreadAvailable
2019-04-22 23:25:16 +02:00
}
2019-04-18 21:36:22 +02:00
2019-07-27 21:49:07 +02:00
func updateProgressIndicatorIfNeeded ( ) {
if ! coordinator . isThreePanelMode {
navigationController ? . updateAccountRefreshProgressIndicator ( )
}
}
2019-04-15 22:03:05 +02:00
func configureTimelineCell ( _ cell : MasterTimelineTableViewCell , article : Article ) {
2019-04-29 12:51:47 +02:00
let avatar = avatarFor ( 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
let showAvatar = coordinator . showAvatars && avatar != nil
2019-04-29 22:29:00 +02:00
cell . cellData = MasterTimelineCellData ( article : article , showFeedName : showFeedNames , feedName : article . feed ? . nameForDisplay , avatar : avatar , showAvatar : showAvatar , featuredImage : featuredImage , numberOfLines : numberOfTextLines )
2019-04-15 22:03:05 +02:00
}
func avatarFor ( _ article : Article ) -> UIImage ? {
2019-06-29 20:35:12 +02:00
if ! coordinator . showAvatars {
2019-04-15 22:03:05 +02:00
return nil
}
if let authors = article . authors {
for author in authors {
2019-05-19 20:03:07 +02:00
if let image = avatarForAuthor ( author ) {
2019-04-15 22:03:05 +02:00
return image
}
}
}
guard let feed = article . feed else {
return nil
}
2019-04-29 12:51:47 +02:00
let feedIconImage = appDelegate . feedIconDownloader . icon ( for : feed )
2019-05-19 20:03:07 +02:00
if feedIconImage != nil {
2019-04-29 12:51:47 +02:00
return feedIconImage
}
2019-06-14 22:33:13 +02:00
if let feed = article . feed , let faviconImage = appDelegate . faviconDownloader . faviconAsAvatar ( for : feed ) {
2019-05-19 20:03:07 +02:00
return faviconImage
2019-04-29 12:51:47 +02:00
}
2019-04-29 14:07:57 +02:00
return FaviconGenerator . favicon ( feed )
2019-04-15 22:03:05 +02:00
}
func avatarForAuthor ( _ author : Author ) -> UIImage ? {
return appDelegate . authorAvatarDownloader . image ( for : author )
}
func featuredImageFor ( _ article : Article ) -> UIImage ? {
if let url = article . imageURL , let data = appDelegate . imageDownloader . image ( for : url ) {
return RSImage ( data : data )
}
return nil
}
func queueReloadVisableCells ( ) {
CoalescingQueue . standard . add ( self , #selector ( reloadAllVisibleCells ) )
}
func performBlockAndRestoreSelection ( _ block : ( ( ) -> Void ) ) {
let indexPaths = tableView . indexPathsForSelectedRows
block ( )
indexPaths ? . forEach { [ weak self ] indexPath in
self ? . tableView . selectRow ( at : indexPath , animated : false , scrollPosition : . none )
}
}
2019-08-16 20:19:06 +02:00
func toggleArticleReadStatusAction ( indexPath : IndexPath ) -> UIAction {
let article = coordinator . articles [ indexPath . row ]
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-08-19 23:03:07 +02:00
self ? . coordinator . toggleRead ( for : indexPath )
2019-08-16 20:19:06 +02:00
}
return action
}
func toggleArticleStarStatusAction ( indexPath : IndexPath ) -> UIAction {
let article = coordinator . articles [ indexPath . row ]
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-08-19 23:03:07 +02:00
self ? . coordinator . toggleStar ( for : indexPath )
2019-08-16 20:19:06 +02:00
}
return action
}
2019-08-19 00:34:53 +02:00
func markOlderAsReadAction ( indexPath : IndexPath ) -> UIAction {
let title = NSLocalizedString ( " Mark Older as Read " , comment : " Mark Older as Read " )
let image = coordinator . sortDirection = = . orderedDescending ? AppAssets . markOlderAsReadDownImage : AppAssets . markOlderAsReadUpImage
let action = UIAction ( title : title , image : image ) { [ weak self ] action in
2019-08-19 23:03:07 +02:00
self ? . coordinator . markAsReadOlderArticlesInTimeline ( indexPath )
2019-08-19 00:34:53 +02:00
}
return action
}
func markOlderAsReadAlertAction ( indexPath : IndexPath , completionHandler : @ escaping ( Bool ) -> Void ) -> UIAlertAction {
let title = NSLocalizedString ( " Mark Older as Read " , comment : " Mark Older as Read " )
let action = UIAlertAction ( title : title , style : . default ) { [ weak self ] action in
2019-08-19 23:03:07 +02:00
self ? . coordinator . markAsReadOlderArticlesInTimeline ( indexPath )
2019-08-19 00:34:53 +02:00
completionHandler ( true )
}
return action
}
2019-08-19 22:45:52 +02:00
func discloseFeedAction ( indexPath : IndexPath ) -> UIAction ? {
guard let feed = coordinator . articles [ indexPath . row ] . feed else {
return nil
}
let title = NSLocalizedString ( " Select Feed " , comment : " Select Feed " )
let action = UIAction ( title : title , image : AppAssets . openInSidebarImage ) { [ weak self ] action in
self ? . coordinator . discloseFeed ( feed )
}
return action
}
func discloseFeedAlertAction ( indexPath : IndexPath , completionHandler : @ escaping ( Bool ) -> Void ) -> UIAlertAction ? {
guard let feed = coordinator . articles [ indexPath . row ] . feed else {
return nil
}
let title = NSLocalizedString ( " Select Feed " , comment : " Select Feed " )
let action = UIAlertAction ( title : title , style : . default ) { [ weak self ] action in
self ? . coordinator . discloseFeed ( feed )
completionHandler ( true )
}
return action
}
2019-08-20 00:26:09 +02:00
func markAllInFeedAsReadAction ( indexPath : IndexPath ) -> UIAction ? {
guard let feed = coordinator . articles [ indexPath . row ] . feed else {
return nil
}
let articles = Array ( feed . fetchArticles ( ) )
guard articles . canMarkAllAsRead ( ) else {
return nil
}
let localizedMenuText = NSLocalizedString ( " Mark All as Read in “%@” " , comment : " Command " )
let title = NSString . localizedStringWithFormat ( localizedMenuText as NSString , feed . nameForDisplay ) as String
let action = UIAction ( title : title , image : AppAssets . markAllInFeedAsReadImage ) { [ weak self ] action in
self ? . coordinator . markAllAsRead ( articles )
}
return action
}
func markAllInFeedAsReadAlertAction ( indexPath : IndexPath , completionHandler : @ escaping ( Bool ) -> Void ) -> UIAlertAction ? {
guard let feed = coordinator . articles [ indexPath . row ] . feed else {
return nil
}
let articles = Array ( feed . fetchArticles ( ) )
guard articles . canMarkAllAsRead ( ) else {
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-08-20 00:26:09 +02:00
let title = NSString . localizedStringWithFormat ( localizedMenuText as NSString , feed . nameForDisplay ) as String
let action = UIAlertAction ( title : title , style : . default ) { [ weak self ] action in
self ? . coordinator . markAllAsRead ( articles )
completionHandler ( true )
}
return action
}
2019-08-20 00:38:30 +02:00
func openInBrowserAction ( indexPath : IndexPath ) -> UIAction ? {
guard let preferredLink = coordinator . articles [ indexPath . row ] . preferredLink , let _ = URL ( string : preferredLink ) else {
return nil
}
let title = NSLocalizedString ( " Open in Browser " , comment : " Open in Browser " )
let action = UIAction ( title : title , image : AppAssets . safariImage ) { [ weak self ] action in
self ? . coordinator . showBrowser ( for : indexPath )
}
return action
}
func openInBrowserAlertAction ( indexPath : IndexPath , completionHandler : @ escaping ( Bool ) -> Void ) -> UIAlertAction ? {
guard let preferredLink = coordinator . articles [ indexPath . row ] . preferredLink , let _ = URL ( string : preferredLink ) else {
return nil
}
let title = NSLocalizedString ( " Open in Browser " , comment : " Open in Browser " )
let action = UIAlertAction ( title : title , style : . default ) { [ weak self ] action in
self ? . coordinator . showBrowser ( for : indexPath )
completionHandler ( true )
}
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 ? ) {
let itemSource = ArticleActivityItemSource ( url : url , subject : title )
let activityViewController = UIActivityViewController ( activityItems : [ itemSource ] , applicationActivities : nil )
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 )
}
func shareAction ( indexPath : IndexPath ) -> UIAction ? {
let article = coordinator . articles [ indexPath . row ]
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
}
func shareAlertAction ( indexPath : IndexPath , completionHandler : @ escaping ( Bool ) -> Void ) -> UIAlertAction ? {
let article = coordinator . articles [ indexPath . row ]
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
completionHandler ( true )
self ? . shareDialogForTableCell ( indexPath : indexPath , url : url , title : article . title )
}
return action
}
2019-04-15 22:03:05 +02:00
}