@@ -28,345 +28,175 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
@ Published var nameForDisplay = " "
@ Published var selectedArticleIDs = Set < String > ( ) // D o n ' t u s e d i r e c t l y . U s e s e l e c t e d A r t i c l e s
@ Published var selectedArticleID : String ? = nil // D o n ' t u s e d i r e c t l y . U s e s e l e c t e d A r t i c l e s
@ Published var selectedArticles = [ Article ] ( )
@ Published var selectedTimelineItems = [ TimelineItem ] ( )
@ Published var readFilterEnabledTable = [ FeedIdentifier : Bool ] ( )
@ Published var isReadFiltered : Bool ? = nil
@ Published var articles = [ Article ] ( ) {
didSet {
articleDictionaryNeedsUpdate = true
}
}
@ Published var timelineItems = [ TimelineItem ] ( ) {
didSet {
timelineItemDictionaryNeedsUpdate = true
}
}
var timelineItemsPublisher : AnyPublisher < [ TimelineItem ] , Never > ?
var selectedTimelineItemsPublisher : AnyPublisher < [ TimelineItem ] , Never > ?
var readFilterEnabledTable = [ FeedIdentifier : Bool ] ( )
// I d o n ' t l i k e t h i s f l a g a n d f e e l l i k e i t i s a h a c k . M a y b e t h e r e i s a b e t t e r w a y t o d o t h i s u s i n g C o m b i n e .
var isSelectNextUnread = false
var undoManager : UndoManager ?
var undoableCommands = [ UndoableCommand ] ( )
private var cancellables = Set < AnyCancellable > ( )
private var feeds = [ Feed ] ( )
private var fetchSerialNumber = 0
private let fetchRequestQueue = FetchRequestQueue ( )
private var exceptionArticleFetcher : ArticleFetcher ?
static let fetchAndMergeArticlesQueue = CoalescingQueue ( name : " Fetch and Merge Articles " , interval : 0.5 , maxInterval : 2.0 )
private var articleDictionaryNeedsUpdate = true
private var _idToArticleDictionary = [ String : Article ] ( )
private var idToArticleDictionary : [ String : Article ] {
if articleDictionaryNeedsUpdate {
rebuildArticleDictionaries ( )
}
return _idToArticleDictionary
}
private var timelineItemDictionaryNeedsUpdate = true
private var _idToTimelineItemDictionary = [ String : Int ] ( )
private var idToTimelineItemDictionary : [ String : Int ] {
if timelineItemDictionaryNeedsUpdate {
rebuildTimelineItemDictionaries ( )
}
return _idToTimelineItemDictionary
}
private var sortDirection = AppDefaults . shared . timelineSortDirection {
didSet {
if sortDirection != oldValue {
sortParametersDidChange ( )
}
}
}
private var sortDirectionSubject = PassthroughSubject < Bool , Never > ( )
private var groupByFeedSubject = PassthroughSubject < Bool , Never > ( )
private var groupByFeed = AppDefaults . shared . timelineGroupByFeed {
didSet {
if groupByFeed != oldValue {
sortParametersDidChange ( )
}
}
}
func startup ( ) {
subscribeToArticleStatusChanges ( )
init ( delegate : TimelineModelDelegate ) {
self . delegate = delegate
// s u b s c r i b e T o A r t i c l e S t a t u s C h a n g e s ( )
subscribeToUserDefaultsChanges ( )
subscribeToSelectedFeed Changes ( )
subscribeToArticleFetch Changes ( )
subscribeToSelectedArticleSelectionChanges ( )
subscribeToAccountDidDownloadArticle s ( )
// s u b s c r i b e T o A c c o u n t D i d D o w n l o a d A r t i c l e s ( )
}
// MARK: S u b s c r i p t i o n s
func subscribeToArticleStatusChange s ( ) {
NotificationCente r .defaul t .publishe r (fo r : . StatusesDidChang e ) .sink { [ weak sel f ] note i n
guard let self = sel f , let articleIDs = not e .userInf o ? [Accoun t .UserInfoKe y .articleID s ] a s ? Se t <Strin g > else {
retur n
}
articleID s .forEach { articleID i n
if let timelineItemIndex = sel f .idToTimelineItemDictionar y [articleI D ] {
sel f .timelineItem s [timelineItemInde x ] .updateStatu s ( )
}
}
} .stor e (i n : & cancellable s )
}
// f u n c s u b s c r i b e T o A r t i c l e S t a t u s C h a n g e s ( ) {
// N o t i f i c a t i o n C e n t e r .d e f a u l t .p u b l i s h e r (f o r : . S t a t u s e s D i d C h a n g e ) .s i n k { [ w e a k s e l f ] n o t e i n
// g u a r d l e t s e l f = s e l f , l e t a r t i c l e I D s = n o t e .u s e r I n f o ? [A c c o u n t .U s e r I n f o K e y .a r t i c l e I D s ] a s ? S e t <S t r i n g > e l s e {
// r e t u r n
// }
// a r t i c l e I D s .f o r E a c h { a r t i c l e I D i n
// i f l e t t i m e l i n e I t e m I n d e x = s e l f .i d T o T i m e l i n e I t e m D i c t i o n a r y [a r t i c l e I D ] {
// s e l f .t i m e l i n e I t e m s [t i m e l i n e I t e m I n d e x ] .u p d a t e S t a t u s ( )
// }
// }
// } .s t o r e (i n : & c a n c e l l a b l e s )
// }
func subscribeToAccountDidDownloadArticle s ( ) {
NotificationCente r .defaul t .publishe r (fo r : . AccountDidDownloadArticle s ) .sink { [ weak sel f ] note i n
guard let self = sel f , let feeds = not e .userInf o ? [Accoun t .UserInfoKe y .webFeed s ] a s ? Se t <WebFee d > else {
retur n
}
if sel f .anySelectedFeedIntersectio n (wit h : feed s ) || sel f .anySelectedFeedIsPseudoFee d ( ) {
sel f .queueFetchAndMergeArticle s ( )
}
} .stor e (i n : & cancellable s )
}
// f u n c s u b s c r i b e T o A c c o u n t D i d D o w n l o a d A r t i c l e s ( ) {
// N o t i f i c a t i o n C e n t e r .d e f a u l t .p u b l i s h e r (f o r : . A c c o u n t D i d D o w n l o a d A r t i c l e s ) .s i n k { [ w e a k s e l f ] n o t e i n
// g u a r d l e t s e l f = s e l f , l e t f e e d s = n o t e .u s e r I n f o ? [A c c o u n t .U s e r I n f o K e y .w e b F e e d s ] a s ? S e t <W e b F e e d > e l s e {
// r e t u r n
// }
// i f s e l f .a n y S e l e c t e d F e e d I n t e r s e c t i o n (w i t h : f e e d s ) | | s e l f .a n y S e l e c t e d F e e d I s P s e u d o F e e d ( ) {
// s e l f .q u e u e F e t c h A n d M e r g e A r t i c l e s ( )
// }
// } .s t o r e (i n : & c a n c e l l a b l e s )
// }
func subscribeToUserDefaultsChanges ( ) {
NotificationCenter . default . publisher ( for : UserDefaults . didChangeNotification ) . sink { [ weak self ] _ in
self ? . sortDirection = AppD efaults . shared . timelineSortDirec tion
self ? . groupByFeed = AppDefaults . shared . timelineGroupByFeed
let kickStartNote = Notification ( name : Notification . Name ( " Kick Start " ) )
NotificationCenter . d efault. publisher ( for : UserDefaults . didChangeNotifica tion)
. prepend ( kickStartNote )
. sink { [ weak self ] _ in
self ? . sortDirectionSubject . send ( AppDefaults . shared . timelineSortDirection )
self ? . groupByFeedSubject . send ( AppDefaults . shared . timelineGroupByFeed )
} . store ( in : & cancellables )
}
func subscribeToSelectedFeed Changes ( ) {
func subscribeToArticleFetch Changes ( ) {
guard let selectedFeedsPublisher = delegate ? . selectedFeedsPublisher else { return }
selectedFeedsPublisher . sink { [ weak self ] feeds in
guard let self = self else { return }
self . feeds = feeds
self . fetchArticles ( )
} . store ( in : & cancellab les )
let sortDirectionPublisher = sortDirectionSubject . removeDuplicates ( )
let groupByPublisher = groupByFeedSubject . removeDuplicates ( )
timelineItemsPublisher = selectedFeedsPublisher
. map { [ weak self ] feeds -> Set < Artic le > in
return self ? . fetchArticles ( feeds : feeds ) ? ? Set < Article > ( )
}
. combineLatest ( $ isReadFiltered , sortDirectionPublisher , groupByPublisher )
. compactMap { [ weak self ] articles , filtered , sortDirection , groupBy -> [ TimelineItem ] in
let sortedArticles = Array ( articles ) . sortedByDate ( sortDirection ? . orderedDescending : . orderedAscending , groupByFeed : groupBy )
return self ? . buildTimelineItems ( articles : sortedArticles ) ? ? [ TimelineItem ] ( )
}
. eraseToAnyPublisher ( )
}
func subscribeToSelectedArticleSelectionChanges ( ) {
$ selectedArticleID s .map { [ weak sel f ] articleIDs i n
return articleID s .compactMap { sel f ? .idToArticleDictionar y [$ 0 ] }
}
. assig n (t o : & $selectedArticle s )
$ selectedArticleI D .compactMap { [ weak sel f ] articleID i n
if let articleID = articleI D , let article = sel f ? .idToArticleDictionar y [articleI D ] {
return [ articl e ]
} else {
return ni l
}
}
. assig n (t o : & $selectedArticle s )
// A s s i g n t h e s e l e c t e d t i m e l i n e i t e m s
$ selectedArticle s .compactMap { [ weak sel f ] selectedArticles i n
return selectedArticle s .compactMap {
if let index = sel f ? .idToTimelineItemDictionar y [$ 0 .articleI D ] {
return sel f ? .timelineItem s [inde x ]
}
return ni l
}
} .assig n (t o : & $selectedTimelineItem s )
// A u t o m a t i c a l l y m a r k a s e l e c t e d r e c o r d a s r e a d
$ selectedArticle s
. filter { $ 0 .count = = 1 }
. compactMap { $ 0 .first }
. filter { ! $ 0 .statu s .read }
. sink { markArticle s (Se t ( [$ 0 ] ) , statusKe y : . rea d , fla g : tru e ) }
. stor e (i n : & cancellable s )
// $ s e l e c t e d A r t i c l e I D s .m a p { [ w e a k s e l f ] a r t i c l e I D s i n
// r e t u r n a r t i c l e I D s .c o m p a c t M a p { s e l f ? .i d T o A r t i c l e D i c t i o n a r y [$ 0 ] }
// }
// . a s s i g n (t o : & $s e l e c t e d A r t i c l e s )
//
// $ s e l e c t e d A r t i c l e I D .c o m p a c t M a p { [ w e a k s e l f ] a r t i c l e I D i n
// i f l e t a r t i c l e I D = a r t i c l e I D , l e t a r t i c l e = s e l f ? .i d T o A r t i c l e D i c t i o n a r y [a r t i c l e I D ] {
// r e t u r n [ a r t i c l e ]
// } e l s e {
// r e t u r n n i l
// }
// }
// . a s s i g n (t o : & $s e l e c t e d A r t i c l e s )
//
// / / A s s i g n t h e s e l e c t e d t i m e l i n e i t e m s
// $ s e l e c t e d A r t i c l e s .c o m p a c t M a p { [ w e a k s e l f ] s e l e c t e d A r t i c l e s i n
// r e t u r n s e l e c t e d A r t i c l e s .c o m p a c t M a p {
// i f l e t i n d e x = s e l f ? .i d T o T i m e l i n e I t e m D i c t i o n a r y [$ 0 .a r t i c l e I D ] {
// r e t u r n s e l f ? .t i m e l i n e I t e m s [i n d e x ]
// }
// r e t u r n n i l
// }
// } .a s s i g n (t o : & $s e l e c t e d T i m e l i n e I t e m s )
//
// / / A u t o m a t i c a l l y m a r k a s e l e c t e d r e c o r d a s r e a d
// $ s e l e c t e d A r t i c l e s
// . f i l t e r { $ 0 .c o u n t = = 1 }
// . c o m p a c t M a p { $ 0 .f i r s t }
// . f i l t e r { ! $ 0 .s t a t u s .r e a d }
// . s i n k { m a r k A r t i c l e s (S e t ( [$ 0 ] ) , s t a t u s K e y : . r e a d , f l a g : t r u e ) }
// . s t o r e (i n : & c a n c e l l a b l e s )
}
// MARK: A P I
func toggleReadFilter ( ) {
guard let filter = isReadFiltere d , let feedID = feed s .firs t ? .feedID else { return }
readFilterEnabledTabl e [feedI D ] = ! filte r
isReadFiltered = ! filte r
sel f .fetchArticle s ( )
// g u a r d l e t f i l t e r = i s R e a d F i l t e r e d , l e t f e e d I D = f e e d s .f i r s t ? .f e e d I D e l s e { r e t u r n }
// r e a d F i l t e r E n a b l e d T a b l e [f e e d I D ] = ! f i l t e r
// i s R e a d F i l t e r e d = ! f i l t e r
// s e l f .f e t c h A r t i c l e s ( )
}
func toggleReadStatusForSelectedArticles ( ) {
guard ! selectedArticle s .isEmpty else {
retur n
}
if selectedArticle s .anyArticleIsUnrea d ( ) {
markSelectedArticlesAsRea d ( )
} else {
markSelectedArticlesAsUnrea d ( )
}
}
func canMarkIndicatedArticlesAsRead ( _ timelineItem : TimelineItem ) -> Bool {
let articles = indicatedTimelineItems ( timelineItem ) . map { $0 . article }
return articles . anyArticleIsUnread ( )
}
func markIndicatedArticlesAsRead ( _ timelineItem : TimelineItem ) {
let articles = indicatedTimelineItems ( timelineItem ) . map { $0 . article }
markArticlesWithUndo ( articles , statusKey : . read , flag : true )
}
func markSelectedArticlesAsRead ( ) {
markArticlesWithUndo ( selectedArticles , statusKey : . read , flag : true )
}
func canMarkIndicatedArticlesAsUnread ( _ timelineItem : TimelineItem ) -> Bool {
let articles = indicatedTimelineItems ( timelineItem ) . map { $0 . article }
return articles . anyArticleIsReadAndCanMarkUnread ( )
}
func markIndicatedArticlesAsUnread ( _ timelineItem : TimelineItem ) {
let articles = indicatedTimelineItems ( timelineItem ) . map { $0 . article }
markArticlesWithUndo ( articles , statusKey : . read , flag : false )
}
func markSelectedArticlesAsUnread ( ) {
markArticlesWithUndo ( selectedArticles , statusKey : . read , flag : false )
}
func canMarkAboveAsRead ( _ timelineItem : TimelineItem ) -> Bool {
let timelineItem = indicatedAboveTimelineItem ( timelineItem )
return articles . articlesAbove ( position : timelineItem . index ) . canMarkAllAsRead ( )
}
func markAboveAsRead ( _ timelineItem : TimelineItem ) {
let timelineItem = indicatedAboveTimelineItem ( timelineItem )
let articlesToMark = articles . articlesAbove ( position : timelineItem . index )
guard ! articlesToMark . isEmpty else { return }
markArticlesWithUndo ( articlesToMark , statusKey : . read , flag : true )
}
func canMarkBelowAsRead ( _ timelineItem : TimelineItem ) -> Bool {
let timelineItem = indicatedBelowTimelineItem ( timelineItem )
return articles . articlesBelow ( position : timelineItem . index ) . canMarkAllAsRead ( )
}
func markBelowAsRead ( _ timelineItem : TimelineItem ) {
let timelineItem = indicatedBelowTimelineItem ( timelineItem )
let articlesToMark = articles . articlesBelow ( position : timelineItem . index )
guard ! articlesToMark . isEmpty else { return }
markArticlesWithUndo ( articlesToMark , statusKey : . read , flag : true )
}
func canMarkAllAsReadInWebFeed ( _ timelineItem : TimelineItem ) -> Bool {
return timelineItem . article . webFeed ? . unreadCount ? ? 0 > 0
}
func markAllAsReadInWebFeed ( _ timelineItem : TimelineItem ) {
guard let articlesSet = try ? timelineItem . article . webFeed ? . fetchArticles ( ) else { return }
let articlesToMark = Array ( articlesSet )
markArticlesWithUndo ( articlesToMark , statusKey : . read , flag : true )
}
func canMarkAllAsRead ( ) -> Bool {
return articles . canMarkAllAsRead ( )
}
func markAllAsRead ( ) {
markArticlesWithUndo ( articles , statusKey : . read , flag : true )
}
func toggleStarredStatusForSelectedArticles ( ) {
guard ! selectedArticles . isEmpty else {
return
}
if selectedArticles . anyArticleIsUnstarred ( ) {
markSelectedArticlesAsStarred ( )
} else {
markSelectedArticlesAsUnstarred ( )
}
}
func canMarkIndicatedArticlesAsStarred ( _ timelineItem : TimelineItem ) -> Bool {
let articles = indicatedTimelineItems ( timelineItem ) . map { $0 . article }
return articles . anyArticleIsUnstarred ( )
}
func markIndicatedArticlesAsStarred ( _ timelineItem : TimelineItem ) {
let articles = indicatedTimelineItems ( timelineItem ) . map { $0 . article }
markArticlesWithUndo ( articles , statusKey : . starred , flag : true )
}
func markSelectedArticlesAsStarred ( ) {
markArticlesWithUndo ( selectedArticles , statusKey : . starred , flag : true )
}
func canMarkIndicatedArticlesAsUnstarred ( _ timelineItem : TimelineItem ) -> Bool {
let articles = indicatedTimelineItems ( timelineItem ) . map { $0 . article }
return articles . anyArticleIsStarred ( )
}
func markIndicatedArticlesAsUnstarred ( _ timelineItem : TimelineItem ) {
let articles = indicatedTimelineItems ( timelineItem ) . map { $0 . article }
markArticlesWithUndo ( articles , statusKey : . starred , flag : false )
}
func markSelectedArticlesAsUnstarred ( ) {
markArticlesWithUndo ( selectedArticles , statusKey : . starred , flag : false )
}
func canOpenIndicatedArticleInBrowser ( _ timelineItem : TimelineItem ) -> Bool {
guard indicatedTimelineItems ( timelineItem ) . count = = 1 else { return false }
return timelineItem . article . preferredLink != nil
}
func openIndicatedArticleInBrowser ( _ timelineItem : TimelineItem ) {
openIndicatedArticleInBrowser ( timelineItem . article )
}
func openIndicatedArticleInBrowser ( _ article : Article ) {
guard let link = article . preferredLink else { return }
#if os ( macOS )
Browser . open ( link , invertPreference : NSApp . currentEvent ? . modifierFlags . contains ( . shift ) ? ? false )
#else
guard let url = URL ( string : link ) else { return }
UIApplication . shared . open ( url , options : [ : ] )
#endif
}
func openSelectedArticleInBrowser ( ) {
guard let article = selectedArticles . first else { return }
openIndicatedArticleInBrowser ( article )
// g u a r d ! s e l e c t e d A r t i c l e s .i s E m p t y e l s e {
// r e t u r n
// }
// i f s e l e c t e d A r t i c l e s .a n y A r t i c l e I s U n r e a d ( ) {
// m a r k S e l e c t e d A r t i c l e s A s R e a d ( )
// } e l s e {
// m a r k S e l e c t e d A r t i c l e s A s U n r e a d ( )
// }
}
@ discardableResult
func goToNextUnread ( ) -> Bool {
var startInde x : In t
if let firstArticle = selectedArticle s .firs t , let index = timelineItem s .firstInde x (wher e : { $ 0 .article = = firstArticle } ) {
startIndex = inde x
} else {
startIndex = 0
}
for i in startInde x . . <timelineItem s .count {
if ! timelineItem s [ i ] .articl e .statu s .read {
selec t (timelineItem s [ i ] .articl e .articleI D )
return tru e
}
}
// v a r s t a r t I n d e x : I n t
// i f l e t f i r s t A r t i c l e = s e l e c t e d A r t i c l e s .f i r s t , l e t i n d e x = t i m e l i n e I t e m s .f i r s t I n d e x (w h e r e : { $ 0 .a r t i c l e = = f i r s t A r t i c l e } ) {
// s t a r t I n d e x = i n d e x
// } e l s e {
// s t a r t I n d e x = 0
// }
//
// f o r i i n s t a r t I n d e x . . <t i m e l i n e I t e m s .c o u n t {
// i f ! t i m e l i n e I t e m s [ i ] .a r t i c l e .s t a t u s .r e a d {
// s e l e c t (t i m e l i n e I t e m s [ i ] .a r t i c l e .a r t i c l e I D )
// r e t u r n t r u e
// }
// }
//
return false
}
func articleFor ( _ articleID : String ) -> Article ? {
return idToArticleDictionary [ articleID ]
return nil
// r e t u r n i d T o A r t i c l e D i c t i o n a r y [ a r t i c l e I D ]
}
func findPrevArticle ( _ article : Article ) -> Article ? {
guard let index = articles . firstIndex ( of : article ) , index > 0 else {
return nil
}
return articles [ index - 1 ]
return nil
// g u a r d l e t i n d e x = a r t i c l e s . f i r s t I n d e x ( o f : a r t i c l e ) , i n d e x > 0 e l s e {
// r e t u r n n i l
// }
// r e t u r n a r t i c l e s [ i n d e x - 1 ]
}
func findNextArticle ( _ article : Article ) -> Article ? {
guard let index = articles . firstIndex ( of : article ) , index + 1 != articles . count else {
return nil
}
return articles [ index + 1 ]
return nil
// g u a r d l e t i n d e x = a r t i c l e s . f i r s t I n d e x ( o f : a r t i c l e ) , i n d e x + 1 ! = a r t i c l e s . c o u n t e l s e {
// r e t u r n n i l
// }
// r e t u r n a r t i c l e s [ i n d e x + 1 ]
}
func selectArticle ( _ article : Article ) {
@@ -379,30 +209,6 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
private extension TimelineModel {
func indicatedTimelineItems ( _ timelineItem : TimelineItem ) -> [ TimelineItem ] {
if selectedTimelineItems . contains ( where : { $0 . id = = timelineItem . id } ) {
return selectedTimelineItems
} else {
return [ timelineItem ]
}
}
func indicatedAboveTimelineItem ( _ timelineItem : TimelineItem ) -> TimelineItem {
if selectedTimelineItems . contains ( where : { $0 . id = = timelineItem . id } ) {
return selectedTimelineItems . first !
} else {
return timelineItem
}
}
func indicatedBelowTimelineItem ( _ timelineItem : TimelineItem ) -> TimelineItem {
if selectedTimelineItems . contains ( where : { $0 . id = = timelineItem . id } ) {
return selectedTimelineItems . last !
} else {
return timelineItem
}
}
func markArticlesWithUndo ( _ articles : [ Article ] , statusKey : ArticleStatus . Key , flag : Bool ) {
if let undoManager = undoManager , let markReadCommand = MarkStatusCommand ( initialArticles : articles , statusKey : statusKey , flag : flag , undoManager : undoManager ) {
runCommand ( markReadCommand )
@@ -411,227 +217,92 @@ private extension TimelineModel {
}
}
func select ( _ articleID : String ) {
selectedArticleIDs = Set ( [ articleID ] )
selectedArticleID = articleID
}
// MARK: T i m e l i n e M a n a g e m e n t
func resetReadFilte r ( ) {
guard feed s .count = = 1 , let timelineFeed = feed s .first else {
isReadFiltered = ni l
retur n
}
guard timelineFee d .defaultReadFilterType != . alwaysRead else {
isReadFiltered = ni l
retur n
}
if let feedID = timelineFee d .feedI D , let readFilterEnabled = readFilterEnabledTabl e [feedI D ] {
isReadFiltered = readFilterEnable d
} else {
isReadFiltered = timelineFee d .defaultReadFilterType = = . rea d
}
}
// f u n c r e s e t R e a d F i l t e r ( ) {
// g u a r d f e e d s .c o u n t = = 1 , l e t t i m e l i n e F e e d = f e e d s .f i r s t e l s e {
// i s R e a d F i l t e r e d = n i l
// r e t u r n
// }
//
// g u a r d t i m e l i n e F e e d .d e f a u l t R e a d F i l t e r T y p e ! = . a l w a y s R e a d e l s e {
// i s R e a d F i l t e r e d = n i l
// r e t u r n
// }
//
// i f l e t f e e d I D = t i m e l i n e F e e d .f e e d I D , l e t r e a d F i l t e r E n a b l e d = r e a d F i l t e r E n a b l e d T a b l e [f e e d I D ] {
// i s R e a d F i l t e r e d = r e a d F i l t e r E n a b l e d
// } e l s e {
// i s R e a d F i l t e r e d = t i m e l i n e F e e d .d e f a u l t R e a d F i l t e r T y p e = = . r e a d
// }
// }
func sortParametersDidChange ( ) {
performBlockAndRestoreSelection {
articles = article s .sortedByDat e (sortDirection ? . orderedDescending : . orderedAscendin g , groupByFee d : groupByFee d )
rebuildTimelineItem s ( )
}
// p e r f o r m B l o c k A n d R e s t o r e S e l e c t i o n {
// a r t i c l e s = a r t i c l e s .s o r t e d B y D a t e (s o r t D i r e c t i o n ? . o r d e r e d D e s c e n d i n g : . o r d e r e d A s c e n d i n g , g r o u p B y F e e d : g r o u p B y F e e d )
// r e b u i l d T i m e l i n e I t e m s ( )
// }
}
func performBlockAndRestoreSelection ( _ block : ( ( ) -> Void ) ) {
let savedArticleIDs = selectedArticleID s
let savedArticleID = selectedArticleI D
// l e t s a v e d A r t i c l e I D s = s e l e c t e d A r t i c l e I D s
// l e t s a v e d A r t i c l e I D = s e l e c t e d A r t i c l e I D
block ( )
selectedArticleIDs = savedArticleID s
selectedArticleID = savedArticleI D
}
func rebuildArticleDictionaries ( ) {
var idDictionary = [ String : Article ] ( )
articles . forEach { article in
idDictionary [ article . articleID ] = article
}
_idToArticleDictionary = idDictionary
articleDictionaryNeedsUpdate = false
}
func rebuildTimelineItemDictionaries ( ) {
var idDictionary = [ String : Int ] ( )
for ( index , timelineItem ) in timelineItems . enumerated ( ) {
idDictionary [ timelineItem . article . articleID ] = index
}
_idToTimelineItemDictionary = idDictionary
timelineItemDictionaryNeedsUpdate = false
// s e l e c t e d A r t i c l e I D s = s a v e d A r t i c l e I D s
// s e l e c t e d A r t i c l e I D = s a v e d A r t i c l e I D
}
// MARK: A r t i c l e F e t c h i n g
func fetchArticles ( ) {
replaceArticles ( with : Set < Article > ( ) )
guard ! feeds . isEmpty else {
nameForDisplay = " "
return
}
if feeds . count = = 1 {
nameForDisplay = feeds . first ! . nameForDisplay
} else {
nameForDisplay = NSLocalizedString ( " Multiple " , comment : " Multiple Feeds " )
}
resetReadFilter ( )
#if os ( macOS )
fetchAndReplaceArticlesSync ( )
#else
fetchAndReplaceArticlesAsync ( )
#endif
}
func fetchAndReplaceArticlesSync ( ) {
// T o b e c a l l e d w h e n t h e u s e r h a s m a d e a c h a n g e o f s e l e c t i o n i n t h e s i d e b a r .
// I t b l o c k s t h e m a i n t h r e a d , s o t h a t t h e r e ’ s n o a s y n c d e l a y ,
// s o t h a t t h e e n t i r e d i s p l a y r e f r e s h e s a t o n c e .
// I t ’ s a b e t t e r u s e r e x p e r i e n c e t h i s w a y .
var fetchers = feeds as [ ArticleFetcher ]
if let fetcher = exceptionArticleFetcher {
fetchers . append ( fetcher )
exceptionArticleFetcher = nil
}
let fetchedArticles = fetchUnsortedArticlesSync ( for : fetchers )
replaceArticles ( with : fetchedArticles )
}
func fetchAndReplaceArticlesAsync ( ) {
var fetchers = feeds as [ ArticleFetcher ]
if let fetcher = exceptionArticleFetcher {
fetchers . append ( fetcher )
exceptionArticleFetcher = nil
}
fetchUnsortedArticlesAsync ( for : fetchers ) { [ weak self ] ( articles ) in
self ? . replaceArticles ( with : articles )
}
}
func cancelPendingAsyncFetches ( ) {
fetchSerialNumber += 1
fetchRequestQueue . cancelAllRequests ( )
}
func fetchUnsortedArticlesSync ( for representedObjects : [ Any ] ) -> Set < Article > {
cancelPendingAsyncFetches ( )
let articleFetchers = representedObjects . compactMap { $0 as ? ArticleFetcher }
if articleFetchers . isEmpty {
func fetchArticles ( feeds : [ Feed ] ) -> Set < Article > {
if feeds . isEmpty {
return Set < Article > ( )
}
var fetchedArticles = Set < Article > ( )
for articleFetcher in articleFetcher s {
for feed in feed s {
if isReadFiltered ? ? true {
if let articles = try ? articleFetcher . fetchUnreadArticles ( ) {
if let articles = try ? feed . fetchUnreadArticles ( ) {
fetchedArticles . formUnion ( articles )
}
} else {
if let articles = try ? articleFetcher . fetchArticles ( ) {
if let articles = try ? feed . fetchArticles ( ) {
fetchedArticles . formUnion ( articles )
}
}
}
return fetchedArticles
}
func fetchUnsortedArticlesAsync ( for representedObjects : [ Any ] , completion : @ escaping ArticleSetBlock ) {
// T h e c a l l b a c k w i l l * n o t * b e c a l l e d i f t h e f e t c h i s n o l o n g e r r e l e v a n t — t h a t i s ,
// i f i t ’ s b e e n s u p e r s e d e d b y a n e w e r f e t c h , o r t h e t i m e l i n e w a s e m p t i e d , e t c . , i t w o n ’ t g e t c a l l e d .
precondition ( Thread . isMainThread )
cancelPendingAsyncFetches ( )
let filtered = isReadFiltered ? ? false
let fetchOperation = FetchRequestOperation ( id : fetchSerialNumber , readFilter : filtered , representedObjects : representedObjects ) { [ weak self ] ( articles , operation ) in
precondition ( Thread . isMainThread )
guard ! operation . isCanceled , let strongSelf = self , operation . id = = strongSelf . fetchSerialNumber else {
return
}
completion ( articles )
}
fetchRequestQueue . add ( fetchOperation )
}
return fetchedArticles
}
func replaceArticles ( with unsortedA rticles: Set < Article > ) {
articles = Array ( unsortedArticles ) . sortedByDate ( sortDirection ? . orderedDescending : . orderedAscending , groupByFeed : groupByFeed )
rebuildTimelineItems ( )
selectedArticleIDs = Set < String > ( )
selectedArticleID = nil
if isSelectNextUnread {
goToNextUnread ( )
isSelectNextUnread = false
}
// TODO: U p d a t e u n r e a d c o u n t s a n d o t h e r i t e m d o n e i n d i d S e t o n A p p K i t
}
func rebuildTimelineItems ( ) {
func buildTimelineItems ( a rticles: [ Article ] ) -> [ TimelineItem ] {
var items = [ TimelineItem ] ( )
for ( index , article ) in articles . enumerated ( ) {
items . append ( TimelineItem ( index : index , article : article ) )
}
timelineItems = items
return items
}
func queueFetchAndMergeArticles ( ) {
TimelineModel . fetchAndMergeArticlesQueue . add ( self , #selector ( fetchAndMergeArticles ) )
}
@objc func fetchAndMergeArticles ( ) {
fetchUnsortedArticlesAsync ( for : feeds ) { [ weak self ] ( unsortedArticles ) in
// M e r g e a r t i c l e s b y a r t i c l e I D . F o r a n y u n i q u e a r t i c l e I D i n c u r r e n t a r t i c l e s , a d d t o u n s o r t e d A r t i c l e s .
guard let strongSelf = self else {
return
}
let unsortedArticleIDs = unsortedArticles . articleIDs ( )
var updatedArticles = unsortedArticles
for article in strongSelf . articles {
if ! unsortedArticleIDs . contains ( article . articleID ) {
updatedArticles . insert ( article )
}
}
strongSelf . performBlockAndRestoreSelection {
strongSelf . replaceArticles ( with : updatedArticles )
}
}
}
func anySelectedFeedIsPseudoFeed ( ) -> Bool {
return feeds . contains ( where : { $0 is PseudoFeed } )
}
func anySelectedFeedIntersection ( with webFeeds : Set < WebFeed > ) -> Bool {
for feed in feeds {
if let selectedWebFeed = feed as ? WebFeed {
for webFeed in webFeeds {
if selectedWebFeed . webFeedID = = webFeed . webFeedID || selectedWebFeed . url = = webFeed . url {
return true
}
}
} else if let folder = feed as ? Folder {
for webFeed in webFeeds {
if folder . hasWebFeed ( with : webFeed . webFeedID ) || folder . hasWebFeed ( withURL : webFeed . url ) {
return true
}
}
}
}
return false
}
// f u n c a n y S e l e c t e d F e e d I s P s e u d o F e e d ( ) - > B o o l {
// r e t u r n f e e d s . c o n t a i n s ( w h e r e : { $ 0 i s P s e u d o F e e d } )
// }
//
// f u n c a n y S e l e c t e d F e e d I n t e r s e c t i o n ( w i t h w e b F e e d s : S e t < W e b F e e d > ) - > B o o l {
// f o r f e e d i n f e e d s {
// i f l e t s e l e c t e d W e b F e e d = f e e d a s ? W e b F e e d {
// f o r w e b F e e d i n w e b F e e d s {
// i f s e l e c t e d W e b F e e d . w e b F e e d I D = = w e b F e e d . w e b F e e d I D | | s e l e c t e d W e b F e e d . u r l = = w e b F e e d . u r l {
// r e t u r n t r u e
// }
// }
// } e l s e i f l e t f o l d e r = f e e d a s ? F o l d e r {
// f o r w e b F e e d i n w e b F e e d s {
// i f f o l d e r . h a s W e b F e e d ( w i t h : w e b F e e d . w e b F e e d I D ) | | f o l d e r . h a s W e b F e e d ( w i t h U R L : w e b F e e d . u r l ) {
// r e t u r n t r u e
// }
// }
// }
// }
// r e t u r n f a l s e
// }
}