mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-02-02 20:16:54 +01:00
Begin refactor of Timeline to use Combine fully
This commit is contained in:
parent
c5d040fa97
commit
953c22f605
@ -25,15 +25,16 @@ final class SceneModel: ObservableObject {
|
|||||||
@Published var accountErrorMessage = ""
|
@Published var accountErrorMessage = ""
|
||||||
|
|
||||||
var selectedArticles: [Article] {
|
var selectedArticles: [Article] {
|
||||||
timelineModel.selectedArticles
|
return [Article]()
|
||||||
|
// timelineModel.selectedArticles
|
||||||
}
|
}
|
||||||
|
|
||||||
private var refreshProgressModel: RefreshProgressModel? = nil
|
private var refreshProgressModel: RefreshProgressModel? = nil
|
||||||
private var articleIconSchemeHandler: ArticleIconSchemeHandler? = nil
|
private var articleIconSchemeHandler: ArticleIconSchemeHandler? = nil
|
||||||
|
|
||||||
private(set) var webViewProvider: WebViewProvider? = nil
|
private(set) var webViewProvider: WebViewProvider? = nil
|
||||||
private(set) var sidebarModel = SidebarModel()
|
private(set) lazy var sidebarModel = SidebarModel(delegate: self)
|
||||||
private(set) var timelineModel = TimelineModel()
|
private(set) lazy var timelineModel = TimelineModel(delegate: self)
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
@ -41,10 +42,6 @@ final class SceneModel: ObservableObject {
|
|||||||
|
|
||||||
/// Prepares the SceneModel to be used in the views
|
/// Prepares the SceneModel to be used in the views
|
||||||
func startup() {
|
func startup() {
|
||||||
sidebarModel.delegate = self
|
|
||||||
timelineModel.delegate = self
|
|
||||||
timelineModel.startup()
|
|
||||||
|
|
||||||
self.articleIconSchemeHandler = ArticleIconSchemeHandler(sceneModel: self)
|
self.articleIconSchemeHandler = ArticleIconSchemeHandler(sceneModel: self)
|
||||||
self.webViewProvider = WebViewProvider(articleIconSchemeHandler: self.articleIconSchemeHandler!)
|
self.webViewProvider = WebViewProvider(articleIconSchemeHandler: self.articleIconSchemeHandler!)
|
||||||
|
|
||||||
@ -56,7 +53,7 @@ final class SceneModel: ObservableObject {
|
|||||||
/// Goes to the next unread item found in Sidebar and Timeline order, top to bottom
|
/// Goes to the next unread item found in Sidebar and Timeline order, top to bottom
|
||||||
func goToNextUnread() {
|
func goToNextUnread() {
|
||||||
if !timelineModel.goToNextUnread() {
|
if !timelineModel.goToNextUnread() {
|
||||||
timelineModel.isSelectNextUnread = true
|
// timelineModel.isSelectNextUnread = true
|
||||||
sidebarModel.selectNextUnread.send()
|
sidebarModel.selectNextUnread.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,22 +62,22 @@ final class SceneModel: ObservableObject {
|
|||||||
|
|
||||||
/// Marks all the articles in the Timeline as read
|
/// Marks all the articles in the Timeline as read
|
||||||
func markAllAsRead() {
|
func markAllAsRead() {
|
||||||
timelineModel.markAllAsRead()
|
// timelineModel.markAllAsRead()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggles the read status for the selected articles
|
/// Toggles the read status for the selected articles
|
||||||
func toggleReadStatusForSelectedArticles() {
|
func toggleReadStatusForSelectedArticles() {
|
||||||
timelineModel.toggleReadStatusForSelectedArticles()
|
// timelineModel.toggleReadStatusForSelectedArticles()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggles the star status for the selected articles
|
/// Toggles the star status for the selected articles
|
||||||
func toggleStarredStatusForSelectedArticles() {
|
func toggleStarredStatusForSelectedArticles() {
|
||||||
timelineModel.toggleStarredStatusForSelectedArticles()
|
// timelineModel.toggleStarredStatusForSelectedArticles()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the selected article in an external browser
|
/// Opens the selected article in an external browser
|
||||||
func openSelectedArticleInBrowser() {
|
func openSelectedArticleInBrowser() {
|
||||||
timelineModel.openSelectedArticleInBrowser()
|
// timelineModel.openSelectedArticleInBrowser()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves the article before the given article in the Timeline
|
/// Retrieves the article before the given article in the Timeline
|
||||||
@ -130,20 +127,20 @@ private extension SceneModel {
|
|||||||
|
|
||||||
// MARK: Subscriptions
|
// MARK: Subscriptions
|
||||||
func subscribeToToolbarChangeEvents() {
|
func subscribeToToolbarChangeEvents() {
|
||||||
NotificationCenter.default.publisher(for: .UnreadCountDidChange)
|
// NotificationCenter.default.publisher(for: .UnreadCountDidChange)
|
||||||
.compactMap { $0.object as? AccountManager }
|
// .compactMap { $0.object as? AccountManager }
|
||||||
.sink { [weak self] accountManager in
|
// .sink { [weak self] accountManager in
|
||||||
self?.updateNextUnreadButtonState(accountManager: accountManager)
|
// self?.updateNextUnreadButtonState(accountManager: accountManager)
|
||||||
}.store(in: &cancellables)
|
// }.store(in: &cancellables)
|
||||||
|
//
|
||||||
let blankNotification = Notification(name: .StatusesDidChange)
|
// let blankNotification = Notification(name: .StatusesDidChange)
|
||||||
let statusesDidChangePublisher = NotificationCenter.default.publisher(for: .StatusesDidChange).prepend(blankNotification)
|
// let statusesDidChangePublisher = NotificationCenter.default.publisher(for: .StatusesDidChange).prepend(blankNotification)
|
||||||
let combinedPublisher = timelineModel.$articles.combineLatest(timelineModel.$selectedArticles, statusesDidChangePublisher)
|
// let combinedPublisher = timelineModel.$articles.combineLatest(timelineModel.$selectedArticles, statusesDidChangePublisher)
|
||||||
|
//
|
||||||
combinedPublisher.sink { [weak self] (articles, selectedArticles, _) in
|
// combinedPublisher.sink { [weak self] (articles, selectedArticles, _) in
|
||||||
self?.updateMarkAllAsReadButtonsState(articles: articles)
|
// self?.updateMarkAllAsReadButtonsState(articles: articles)
|
||||||
self?.updateArticleButtonsState(selectedArticles: selectedArticles)
|
// self?.updateArticleButtonsState(selectedArticles: selectedArticles)
|
||||||
}.store(in: &cancellables)
|
// }.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Button State Updates
|
// MARK: Button State Updates
|
||||||
|
@ -37,7 +37,8 @@ class SidebarModel: ObservableObject, UndoableCommandRunner {
|
|||||||
var undoManager: UndoManager?
|
var undoManager: UndoManager?
|
||||||
var undoableCommands = [UndoableCommand]()
|
var undoableCommands = [UndoableCommand]()
|
||||||
|
|
||||||
init() {
|
init(delegate: SidebarModelDelegate) {
|
||||||
|
self.delegate = delegate
|
||||||
subscribeToSelectedFeedChanges()
|
subscribeToSelectedFeedChanges()
|
||||||
subscribeToRebuildSidebarItemsEvents()
|
subscribeToRebuildSidebarItemsEvents()
|
||||||
subscribeToNextUnread()
|
subscribeToNextUnread()
|
||||||
|
@ -15,95 +15,97 @@ struct TimelineContextMenu: View {
|
|||||||
|
|
||||||
@ViewBuilder var body: some View {
|
@ViewBuilder var body: some View {
|
||||||
|
|
||||||
if timelineModel.canMarkIndicatedArticlesAsRead(timelineItem) {
|
Button("Coming back soon...", action: {})
|
||||||
Button {
|
|
||||||
timelineModel.markIndicatedArticlesAsRead(timelineItem)
|
|
||||||
} label: {
|
|
||||||
Text("Mark as Read")
|
|
||||||
#if os(iOS)
|
|
||||||
AppAssets.readOpenImage
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if timelineModel.canMarkIndicatedArticlesAsUnread(timelineItem) {
|
// if timelineModel.canMarkIndicatedArticlesAsRead(timelineItem) {
|
||||||
Button {
|
// Button {
|
||||||
timelineModel.markIndicatedArticlesAsUnread(timelineItem)
|
// timelineModel.markIndicatedArticlesAsRead(timelineItem)
|
||||||
} label: {
|
// } label: {
|
||||||
Text("Mark as Unread")
|
// Text("Mark as Read")
|
||||||
#if os(iOS)
|
// #if os(iOS)
|
||||||
AppAssets.readClosedImage
|
// AppAssets.readOpenImage
|
||||||
#endif
|
// #endif
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if timelineModel.canMarkIndicatedArticlesAsStarred(timelineItem) {
|
// if timelineModel.canMarkIndicatedArticlesAsUnread(timelineItem) {
|
||||||
Button {
|
// Button {
|
||||||
timelineModel.markIndicatedArticlesAsStarred(timelineItem)
|
// timelineModel.markIndicatedArticlesAsUnread(timelineItem)
|
||||||
} label: {
|
// } label: {
|
||||||
Text("Mark as Starred")
|
// Text("Mark as Unread")
|
||||||
#if os(iOS)
|
// #if os(iOS)
|
||||||
AppAssets.starClosedImage
|
// AppAssets.readClosedImage
|
||||||
#endif
|
// #endif
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if timelineModel.canMarkIndicatedArticlesAsUnstarred(timelineItem) {
|
// if timelineModel.canMarkIndicatedArticlesAsStarred(timelineItem) {
|
||||||
Button {
|
// Button {
|
||||||
timelineModel.markIndicatedArticlesAsUnstarred(timelineItem)
|
// timelineModel.markIndicatedArticlesAsStarred(timelineItem)
|
||||||
} label: {
|
// } label: {
|
||||||
Text("Mark as Unstarred")
|
// Text("Mark as Starred")
|
||||||
#if os(iOS)
|
// #if os(iOS)
|
||||||
AppAssets.starOpenImage
|
// AppAssets.starClosedImage
|
||||||
#endif
|
// #endif
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if timelineModel.canMarkAboveAsRead(timelineItem) {
|
// if timelineModel.canMarkIndicatedArticlesAsUnstarred(timelineItem) {
|
||||||
Button {
|
// Button {
|
||||||
timelineModel.markAboveAsRead(timelineItem)
|
// timelineModel.markIndicatedArticlesAsUnstarred(timelineItem)
|
||||||
} label: {
|
// } label: {
|
||||||
Text("Mark Above as Read")
|
// Text("Mark as Unstarred")
|
||||||
#if os(iOS)
|
// #if os(iOS)
|
||||||
AppAssets.markAboveAsReadImage
|
// AppAssets.starOpenImage
|
||||||
#endif
|
// #endif
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if timelineModel.canMarkBelowAsRead(timelineItem) {
|
// if timelineModel.canMarkAboveAsRead(timelineItem) {
|
||||||
Button {
|
// Button {
|
||||||
timelineModel.markBelowAsRead(timelineItem)
|
// timelineModel.markAboveAsRead(timelineItem)
|
||||||
} label: {
|
// } label: {
|
||||||
Text("Mark Below As Read")
|
// Text("Mark Above as Read")
|
||||||
#if os(iOS)
|
// #if os(iOS)
|
||||||
AppAssets.markBelowAsReadImage
|
// AppAssets.markAboveAsReadImage
|
||||||
#endif
|
// #endif
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if timelineModel.canMarkAllAsReadInWebFeed(timelineItem) {
|
// if timelineModel.canMarkBelowAsRead(timelineItem) {
|
||||||
Divider()
|
// Button {
|
||||||
Button {
|
// timelineModel.markBelowAsRead(timelineItem)
|
||||||
timelineModel.markAllAsReadInWebFeed(timelineItem)
|
// } label: {
|
||||||
} label: {
|
// Text("Mark Below As Read")
|
||||||
Text("Mark All as Read in “\(timelineItem.article.webFeed?.nameForDisplay ?? "")”")
|
// #if os(iOS)
|
||||||
#if os(iOS)
|
// AppAssets.markBelowAsReadImage
|
||||||
AppAssets.markAllAsReadImage
|
// #endif
|
||||||
#endif
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// if timelineModel.canMarkAllAsReadInWebFeed(timelineItem) {
|
||||||
if timelineModel.canOpenIndicatedArticleInBrowser(timelineItem) {
|
// Divider()
|
||||||
Divider()
|
// Button {
|
||||||
Button {
|
// timelineModel.markAllAsReadInWebFeed(timelineItem)
|
||||||
timelineModel.openIndicatedArticleInBrowser(timelineItem)
|
// } label: {
|
||||||
} label: {
|
// Text("Mark All as Read in “\(timelineItem.article.webFeed?.nameForDisplay ?? "")”")
|
||||||
Text("Open in Browser")
|
// #if os(iOS)
|
||||||
#if os(iOS)
|
// AppAssets.markAllAsReadImage
|
||||||
AppAssets.openInBrowserImage
|
// #endif
|
||||||
#endif
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// if timelineModel.canOpenIndicatedArticleInBrowser(timelineItem) {
|
||||||
|
// Divider()
|
||||||
|
// Button {
|
||||||
|
// timelineModel.openIndicatedArticleInBrowser(timelineItem)
|
||||||
|
// } label: {
|
||||||
|
// Text("Open in Browser")
|
||||||
|
// #if os(iOS)
|
||||||
|
// AppAssets.openInBrowserImage
|
||||||
|
// #endif
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,345 +28,175 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
|||||||
@Published var nameForDisplay = ""
|
@Published var nameForDisplay = ""
|
||||||
@Published var selectedArticleIDs = Set<String>() // Don't use directly. Use selectedArticles
|
@Published var selectedArticleIDs = Set<String>() // Don't use directly. Use selectedArticles
|
||||||
@Published var selectedArticleID: String? = nil // Don't use directly. Use selectedArticles
|
@Published var selectedArticleID: String? = nil // Don't use directly. Use selectedArticles
|
||||||
@Published var selectedArticles = [Article]()
|
|
||||||
@Published var selectedTimelineItems = [TimelineItem]()
|
|
||||||
@Published var readFilterEnabledTable = [FeedIdentifier: Bool]()
|
|
||||||
@Published var isReadFiltered: Bool? = nil
|
@Published var isReadFiltered: Bool? = nil
|
||||||
|
|
||||||
@Published var articles = [Article]() {
|
var timelineItemsPublisher: AnyPublisher<[TimelineItem], Never>?
|
||||||
didSet {
|
var selectedTimelineItemsPublisher: AnyPublisher<[TimelineItem], Never>?
|
||||||
articleDictionaryNeedsUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Published var timelineItems = [TimelineItem]() {
|
var readFilterEnabledTable = [FeedIdentifier: Bool]()
|
||||||
didSet {
|
|
||||||
timelineItemDictionaryNeedsUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// I don't like this flag and feel like it is a hack. Maybe there is a better way to do this using Combine.
|
|
||||||
var isSelectNextUnread = false
|
|
||||||
|
|
||||||
var undoManager: UndoManager?
|
var undoManager: UndoManager?
|
||||||
var undoableCommands = [UndoableCommand]()
|
var undoableCommands = [UndoableCommand]()
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
private var feeds = [Feed]()
|
private var sortDirectionSubject = PassthroughSubject<Bool, Never>()
|
||||||
private var fetchSerialNumber = 0
|
private var groupByFeedSubject = PassthroughSubject<Bool, Never>()
|
||||||
private let fetchRequestQueue = FetchRequestQueue()
|
|
||||||
private var exceptionArticleFetcher: ArticleFetcher?
|
|
||||||
|
|
||||||
static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5, maxInterval: 2.0)
|
init(delegate: TimelineModelDelegate) {
|
||||||
|
self.delegate = delegate
|
||||||
private var articleDictionaryNeedsUpdate = true
|
// subscribeToArticleStatusChanges()
|
||||||
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 groupByFeed = AppDefaults.shared.timelineGroupByFeed {
|
|
||||||
didSet {
|
|
||||||
if groupByFeed != oldValue {
|
|
||||||
sortParametersDidChange()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startup() {
|
|
||||||
subscribeToArticleStatusChanges()
|
|
||||||
subscribeToUserDefaultsChanges()
|
subscribeToUserDefaultsChanges()
|
||||||
subscribeToSelectedFeedChanges()
|
subscribeToArticleFetchChanges()
|
||||||
subscribeToSelectedArticleSelectionChanges()
|
subscribeToSelectedArticleSelectionChanges()
|
||||||
subscribeToAccountDidDownloadArticles()
|
// subscribeToAccountDidDownloadArticles()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Subscriptions
|
// MARK: Subscriptions
|
||||||
|
|
||||||
func subscribeToArticleStatusChanges() {
|
// func subscribeToArticleStatusChanges() {
|
||||||
NotificationCenter.default.publisher(for: .StatusesDidChange).sink { [weak self] note in
|
// NotificationCenter.default.publisher(for: .StatusesDidChange).sink { [weak self] note in
|
||||||
guard let self = self, let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String> else {
|
// guard let self = self, let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String> else {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
articleIDs.forEach { articleID in
|
// articleIDs.forEach { articleID in
|
||||||
if let timelineItemIndex = self.idToTimelineItemDictionary[articleID] {
|
// if let timelineItemIndex = self.idToTimelineItemDictionary[articleID] {
|
||||||
self.timelineItems[timelineItemIndex].updateStatus()
|
// self.timelineItems[timelineItemIndex].updateStatus()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}.store(in: &cancellables)
|
// }.store(in: &cancellables)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func subscribeToAccountDidDownloadArticles() {
|
// func subscribeToAccountDidDownloadArticles() {
|
||||||
NotificationCenter.default.publisher(for: .AccountDidDownloadArticles).sink { [weak self] note in
|
// NotificationCenter.default.publisher(for: .AccountDidDownloadArticles).sink { [weak self] note in
|
||||||
guard let self = self, let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<WebFeed> else {
|
// guard let self = self, let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<WebFeed> else {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
if self.anySelectedFeedIntersection(with: feeds) || self.anySelectedFeedIsPseudoFeed() {
|
// if self.anySelectedFeedIntersection(with: feeds) || self.anySelectedFeedIsPseudoFeed() {
|
||||||
self.queueFetchAndMergeArticles()
|
// self.queueFetchAndMergeArticles()
|
||||||
}
|
// }
|
||||||
}.store(in: &cancellables)
|
// }.store(in: &cancellables)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func subscribeToUserDefaultsChanges() {
|
func subscribeToUserDefaultsChanges() {
|
||||||
NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification).sink { [weak self] _ in
|
let kickStartNote = Notification(name: Notification.Name("Kick Start"))
|
||||||
self?.sortDirection = AppDefaults.shared.timelineSortDirection
|
NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)
|
||||||
self?.groupByFeed = AppDefaults.shared.timelineGroupByFeed
|
.prepend(kickStartNote)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.sortDirectionSubject.send(AppDefaults.shared.timelineSortDirection)
|
||||||
|
self?.groupByFeedSubject.send(AppDefaults.shared.timelineGroupByFeed)
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribeToSelectedFeedChanges() {
|
func subscribeToArticleFetchChanges() {
|
||||||
guard let selectedFeedsPublisher = delegate?.selectedFeedsPublisher else { return }
|
guard let selectedFeedsPublisher = delegate?.selectedFeedsPublisher else { return }
|
||||||
selectedFeedsPublisher.sink { [weak self] feeds in
|
let sortDirectionPublisher = sortDirectionSubject.removeDuplicates()
|
||||||
guard let self = self else { return }
|
let groupByPublisher = groupByFeedSubject.removeDuplicates()
|
||||||
self.feeds = feeds
|
|
||||||
self.fetchArticles()
|
timelineItemsPublisher = selectedFeedsPublisher
|
||||||
}.store(in: &cancellables)
|
.map { [weak self] feeds -> Set<Article> 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() {
|
func subscribeToSelectedArticleSelectionChanges() {
|
||||||
$selectedArticleIDs.map { [weak self] articleIDs in
|
// $selectedArticleIDs.map { [weak self] articleIDs in
|
||||||
return articleIDs.compactMap { self?.idToArticleDictionary[$0] }
|
// return articleIDs.compactMap { self?.idToArticleDictionary[$0] }
|
||||||
}
|
// }
|
||||||
.assign(to: &$selectedArticles)
|
// .assign(to: &$selectedArticles)
|
||||||
|
//
|
||||||
$selectedArticleID.compactMap { [weak self] articleID in
|
// $selectedArticleID.compactMap { [weak self] articleID in
|
||||||
if let articleID = articleID, let article = self?.idToArticleDictionary[articleID] {
|
// if let articleID = articleID, let article = self?.idToArticleDictionary[articleID] {
|
||||||
return [article]
|
// return [article]
|
||||||
} else {
|
// } else {
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
.assign(to: &$selectedArticles)
|
// .assign(to: &$selectedArticles)
|
||||||
|
//
|
||||||
// Assign the selected timeline items
|
// // Assign the selected timeline items
|
||||||
$selectedArticles.compactMap { [weak self] selectedArticles in
|
// $selectedArticles.compactMap { [weak self] selectedArticles in
|
||||||
return selectedArticles.compactMap {
|
// return selectedArticles.compactMap {
|
||||||
if let index = self?.idToTimelineItemDictionary[$0.articleID] {
|
// if let index = self?.idToTimelineItemDictionary[$0.articleID] {
|
||||||
return self?.timelineItems[index]
|
// return self?.timelineItems[index]
|
||||||
}
|
// }
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
}.assign(to: &$selectedTimelineItems)
|
// }.assign(to: &$selectedTimelineItems)
|
||||||
|
//
|
||||||
// Automatically mark a selected record as read
|
// // Automatically mark a selected record as read
|
||||||
$selectedArticles
|
// $selectedArticles
|
||||||
.filter { $0.count == 1 }
|
// .filter { $0.count == 1 }
|
||||||
.compactMap { $0.first }
|
// .compactMap { $0.first }
|
||||||
.filter { !$0.status.read }
|
// .filter { !$0.status.read }
|
||||||
.sink { markArticles(Set([$0]), statusKey: .read, flag: true) }
|
// .sink { markArticles(Set([$0]), statusKey: .read, flag: true) }
|
||||||
.store(in: &cancellables)
|
// .store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: API
|
// MARK: API
|
||||||
|
|
||||||
func toggleReadFilter() {
|
func toggleReadFilter() {
|
||||||
guard let filter = isReadFiltered, let feedID = feeds.first?.feedID else { return }
|
// guard let filter = isReadFiltered, let feedID = feeds.first?.feedID else { return }
|
||||||
readFilterEnabledTable[feedID] = !filter
|
// readFilterEnabledTable[feedID] = !filter
|
||||||
isReadFiltered = !filter
|
// isReadFiltered = !filter
|
||||||
self.fetchArticles()
|
// self.fetchArticles()
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleReadStatusForSelectedArticles() {
|
func toggleReadStatusForSelectedArticles() {
|
||||||
guard !selectedArticles.isEmpty else {
|
// guard !selectedArticles.isEmpty else {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
if selectedArticles.anyArticleIsUnread() {
|
// if selectedArticles.anyArticleIsUnread() {
|
||||||
markSelectedArticlesAsRead()
|
// markSelectedArticlesAsRead()
|
||||||
} else {
|
// } else {
|
||||||
markSelectedArticlesAsUnread()
|
// markSelectedArticlesAsUnread()
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func goToNextUnread() -> Bool {
|
func goToNextUnread() -> Bool {
|
||||||
var startIndex: Int
|
// var startIndex: Int
|
||||||
if let firstArticle = selectedArticles.first, let index = timelineItems.firstIndex(where: { $0.article == firstArticle }) {
|
// if let firstArticle = selectedArticles.first, let index = timelineItems.firstIndex(where: { $0.article == firstArticle }) {
|
||||||
startIndex = index
|
// startIndex = index
|
||||||
} else {
|
// } else {
|
||||||
startIndex = 0
|
// startIndex = 0
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
for i in startIndex..<timelineItems.count {
|
// for i in startIndex..<timelineItems.count {
|
||||||
if !timelineItems[i].article.status.read {
|
// if !timelineItems[i].article.status.read {
|
||||||
select(timelineItems[i].article.articleID)
|
// select(timelineItems[i].article.articleID)
|
||||||
return true
|
// return true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func articleFor(_ articleID: String) -> Article? {
|
func articleFor(_ articleID: String) -> Article? {
|
||||||
return idToArticleDictionary[articleID]
|
return nil
|
||||||
|
// return idToArticleDictionary[articleID]
|
||||||
}
|
}
|
||||||
|
|
||||||
func findPrevArticle(_ article: Article) -> Article? {
|
func findPrevArticle(_ article: Article) -> Article? {
|
||||||
guard let index = articles.firstIndex(of: article), index > 0 else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
// guard let index = articles.firstIndex(of: article), index > 0 else {
|
||||||
return articles[index - 1]
|
// return nil
|
||||||
|
// }
|
||||||
|
// return articles[index - 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func findNextArticle(_ article: Article) -> Article? {
|
func findNextArticle(_ article: Article) -> Article? {
|
||||||
guard let index = articles.firstIndex(of: article), index + 1 != articles.count else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
// guard let index = articles.firstIndex(of: article), index + 1 != articles.count else {
|
||||||
return articles[index + 1]
|
// return nil
|
||||||
|
// }
|
||||||
|
// return articles[index + 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectArticle(_ article: Article) {
|
func selectArticle(_ article: Article) {
|
||||||
@ -379,30 +209,6 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
|||||||
|
|
||||||
private extension TimelineModel {
|
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) {
|
func markArticlesWithUndo(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool) {
|
||||||
if let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager) {
|
if let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager) {
|
||||||
runCommand(markReadCommand)
|
runCommand(markReadCommand)
|
||||||
@ -411,227 +217,92 @@ private extension TimelineModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(_ articleID: String) {
|
|
||||||
selectedArticleIDs = Set([articleID])
|
|
||||||
selectedArticleID = articleID
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Timeline Management
|
// MARK: Timeline Management
|
||||||
|
|
||||||
func resetReadFilter() {
|
// func resetReadFilter() {
|
||||||
guard feeds.count == 1, let timelineFeed = feeds.first else {
|
// guard feeds.count == 1, let timelineFeed = feeds.first else {
|
||||||
isReadFiltered = nil
|
// isReadFiltered = nil
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
guard timelineFeed.defaultReadFilterType != .alwaysRead else {
|
// guard timelineFeed.defaultReadFilterType != .alwaysRead else {
|
||||||
isReadFiltered = nil
|
// isReadFiltered = nil
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if let feedID = timelineFeed.feedID, let readFilterEnabled = readFilterEnabledTable[feedID] {
|
// if let feedID = timelineFeed.feedID, let readFilterEnabled = readFilterEnabledTable[feedID] {
|
||||||
isReadFiltered = readFilterEnabled
|
// isReadFiltered = readFilterEnabled
|
||||||
} else {
|
// } else {
|
||||||
isReadFiltered = timelineFeed.defaultReadFilterType == .read
|
// isReadFiltered = timelineFeed.defaultReadFilterType == .read
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
func sortParametersDidChange() {
|
func sortParametersDidChange() {
|
||||||
performBlockAndRestoreSelection {
|
// performBlockAndRestoreSelection {
|
||||||
articles = articles.sortedByDate(sortDirection ? .orderedDescending : .orderedAscending, groupByFeed: groupByFeed)
|
// articles = articles.sortedByDate(sortDirection ? .orderedDescending : .orderedAscending, groupByFeed: groupByFeed)
|
||||||
rebuildTimelineItems()
|
// rebuildTimelineItems()
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
|
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
|
||||||
let savedArticleIDs = selectedArticleIDs
|
// let savedArticleIDs = selectedArticleIDs
|
||||||
let savedArticleID = selectedArticleID
|
// let savedArticleID = selectedArticleID
|
||||||
block()
|
block()
|
||||||
selectedArticleIDs = savedArticleIDs
|
// selectedArticleIDs = savedArticleIDs
|
||||||
selectedArticleID = savedArticleID
|
// selectedArticleID = savedArticleID
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Article Fetching
|
// MARK: Article Fetching
|
||||||
|
|
||||||
func fetchArticles() {
|
func fetchArticles(feeds: [Feed]) -> Set<Article> {
|
||||||
replaceArticles(with: Set<Article>())
|
if feeds.isEmpty {
|
||||||
|
|
||||||
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() {
|
|
||||||
// To be called when the user has made a change of selection in the sidebar.
|
|
||||||
// It blocks the main thread, so that there’s no async delay,
|
|
||||||
// so that the entire display refreshes at once.
|
|
||||||
// It’s a better user experience this way.
|
|
||||||
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 {
|
|
||||||
return Set<Article>()
|
return Set<Article>()
|
||||||
}
|
}
|
||||||
|
|
||||||
var fetchedArticles = Set<Article>()
|
var fetchedArticles = Set<Article>()
|
||||||
for articleFetcher in articleFetchers {
|
for feed in feeds {
|
||||||
if isReadFiltered ?? true {
|
if isReadFiltered ?? true {
|
||||||
if let articles = try? articleFetcher.fetchUnreadArticles() {
|
if let articles = try? feed.fetchUnreadArticles() {
|
||||||
fetchedArticles.formUnion(articles)
|
fetchedArticles.formUnion(articles)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let articles = try? articleFetcher.fetchArticles() {
|
if let articles = try? feed.fetchArticles() {
|
||||||
fetchedArticles.formUnion(articles)
|
fetchedArticles.formUnion(articles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchedArticles
|
return fetchedArticles
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUnsortedArticlesAsync(for representedObjects: [Any], completion: @escaping ArticleSetBlock) {
|
func buildTimelineItems(articles: [Article]) -> [TimelineItem] {
|
||||||
// The callback will *not* be called if the fetch is no longer relevant — that is,
|
|
||||||
// if it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t get called.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func replaceArticles(with unsortedArticles: Set<Article>) {
|
|
||||||
articles = Array(unsortedArticles).sortedByDate(sortDirection ? .orderedDescending : .orderedAscending, groupByFeed: groupByFeed)
|
|
||||||
rebuildTimelineItems()
|
|
||||||
|
|
||||||
selectedArticleIDs = Set<String>()
|
|
||||||
selectedArticleID = nil
|
|
||||||
|
|
||||||
if isSelectNextUnread {
|
|
||||||
goToNextUnread()
|
|
||||||
isSelectNextUnread = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Update unread counts and other item done in didSet on AppKit
|
|
||||||
}
|
|
||||||
|
|
||||||
func rebuildTimelineItems() {
|
|
||||||
var items = [TimelineItem]()
|
var items = [TimelineItem]()
|
||||||
for (index, article) in articles.enumerated() {
|
for (index, article) in articles.enumerated() {
|
||||||
items.append(TimelineItem(index: index, article: article))
|
items.append(TimelineItem(index: index, article: article))
|
||||||
}
|
}
|
||||||
timelineItems = items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
func queueFetchAndMergeArticles() {
|
// func anySelectedFeedIsPseudoFeed() -> Bool {
|
||||||
TimelineModel.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles))
|
// return feeds.contains(where: { $0 is PseudoFeed})
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@objc func fetchAndMergeArticles() {
|
// func anySelectedFeedIntersection(with webFeeds: Set<WebFeed>) -> Bool {
|
||||||
|
// for feed in feeds {
|
||||||
fetchUnsortedArticlesAsync(for: feeds) { [weak self] (unsortedArticles) in
|
// if let selectedWebFeed = feed as? WebFeed {
|
||||||
// Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles.
|
// for webFeed in webFeeds {
|
||||||
guard let strongSelf = self else {
|
// if selectedWebFeed.webFeedID == webFeed.webFeedID || selectedWebFeed.url == webFeed.url {
|
||||||
return
|
// return true
|
||||||
}
|
// }
|
||||||
let unsortedArticleIDs = unsortedArticles.articleIDs()
|
// }
|
||||||
var updatedArticles = unsortedArticles
|
// } else if let folder = feed as? Folder {
|
||||||
for article in strongSelf.articles {
|
// for webFeed in webFeeds {
|
||||||
if !unsortedArticleIDs.contains(article.articleID) {
|
// if folder.hasWebFeed(with: webFeed.webFeedID) || folder.hasWebFeed(withURL: webFeed.url) {
|
||||||
updatedArticles.insert(article)
|
// return true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
strongSelf.performBlockAndRestoreSelection {
|
// }
|
||||||
strongSelf.replaceArticles(with: updatedArticles)
|
// }
|
||||||
}
|
// return false
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import SwiftUI
|
|||||||
struct TimelineView: View {
|
struct TimelineView: View {
|
||||||
|
|
||||||
@EnvironmentObject private var timelineModel: TimelineModel
|
@EnvironmentObject private var timelineModel: TimelineModel
|
||||||
|
@State private var timelineItems = [TimelineItem]()
|
||||||
@State private var timelineItemFrames = [String: CGRect]()
|
@State private var timelineItemFrames = [String: CGRect]()
|
||||||
|
|
||||||
@ViewBuilder var body: some View {
|
@ViewBuilder var body: some View {
|
||||||
@ -37,7 +38,7 @@ struct TimelineView: View {
|
|||||||
.help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles")
|
.help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles")
|
||||||
}
|
}
|
||||||
ScrollViewReader { scrollViewProxy in
|
ScrollViewReader { scrollViewProxy in
|
||||||
List(timelineModel.timelineItems, selection: $timelineModel.selectedArticleIDs) { timelineItem in
|
List(timelineItems, selection: $timelineModel.selectedArticleIDs) { timelineItem in
|
||||||
let selected = timelineModel.selectedArticleIDs.contains(timelineItem.article.articleID)
|
let selected = timelineModel.selectedArticleIDs.contains(timelineItem.article.articleID)
|
||||||
TimelineItemView(selected: selected, width: geometryReaderProxy.size.width, timelineItem: timelineItem)
|
TimelineItemView(selected: selected, width: geometryReaderProxy.size.width, timelineItem: timelineItem)
|
||||||
.background(TimelineItemFramePreferenceView(timelineItem: timelineItem))
|
.background(TimelineItemFramePreferenceView(timelineItem: timelineItem))
|
||||||
@ -61,6 +62,11 @@ struct TimelineView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(timelineModel.timelineItemsPublisher!) { items in
|
||||||
|
withAnimation {
|
||||||
|
timelineItems = items
|
||||||
|
}
|
||||||
|
}
|
||||||
.navigationTitle(Text(verbatim: timelineModel.nameForDisplay))
|
.navigationTitle(Text(verbatim: timelineModel.nameForDisplay))
|
||||||
#else
|
#else
|
||||||
ScrollViewReader { scrollViewProxy in
|
ScrollViewReader { scrollViewProxy in
|
||||||
|
@ -80,9 +80,9 @@ class WebViewController: NSViewController {
|
|||||||
statusBarView.heightAnchor.constraint(equalToConstant: 20)
|
statusBarView.heightAnchor.constraint(equalToConstant: 20)
|
||||||
])
|
])
|
||||||
|
|
||||||
selectedArticlesCancellable = sceneModel?.timelineModel.$selectedArticles.sink { [weak self] articles in
|
// selectedArticlesCancellable = sceneModel?.timelineModel.$selectedArticles.sink { [weak self] articles in
|
||||||
self?.articles = articles
|
// self?.articles = articles
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Notifications
|
// MARK: Notifications
|
||||||
|
Loading…
x
Reference in New Issue
Block a user