diff --git a/Multiplatform/Shared/SwiftUI Extensions/HiddenModifier.swift b/Multiplatform/Shared/SwiftUI Extensions/HiddenModifier.swift new file mode 100644 index 000000000..c59d23f59 --- /dev/null +++ b/Multiplatform/Shared/SwiftUI Extensions/HiddenModifier.swift @@ -0,0 +1,21 @@ +// +// HiddenModifier.swift +// NetNewsWire +// +// Created by Maurice Parker on 7/12/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import SwiftUI + +extension View { + func hidden(_ hide: Bool) -> some View { + Group { + if hide { + self.hidden() + } else { + self + } + } + } +} diff --git a/Multiplatform/Shared/Timeline/TimelineModel.swift b/Multiplatform/Shared/Timeline/TimelineModel.swift index b05879ae6..5a9fb6424 100644 --- a/Multiplatform/Shared/Timeline/TimelineModel.swift +++ b/Multiplatform/Shared/Timeline/TimelineModel.swift @@ -25,7 +25,8 @@ class TimelineModel: ObservableObject, UndoableCommandRunner { @Published var selectedArticleIDs = Set() // Don't use directly. Use selectedArticles @Published var selectedArticleID: String? = .none // Don't use directly. Use selectedArticles @Published var selectedArticles = [Article]() - @Published var isReadFiltered = false + @Published var readFilterEnabledTable = [FeedIdentifier: Bool]() + @Published var isReadFiltered: Bool? = nil var undoManager: UndoManager? var undoableCommands = [UndoableCommand]() @@ -33,8 +34,8 @@ class TimelineModel: ObservableObject, UndoableCommandRunner { private var selectedArticleIDsCancellable: AnyCancellable? private var selectedArticleIDCancellable: AnyCancellable? private var selectedArticlesCancellable: AnyCancellable? - private var selectedReadFilteredCancellable: AnyCancellable? + private var feeds = [Feed]() private var fetchSerialNumber = 0 private let fetchRequestQueue = FetchRequestQueue() private var exceptionArticleFetcher: ArticleFetcher? @@ -97,23 +98,30 @@ class TimelineModel: ObservableObject, UndoableCommandRunner { } } - selectedReadFilteredCancellable = $isReadFiltered.sink { [weak self] filter in - guard let self = self else { return } - self.rebuildTimelineItems(isReadFiltered: filter) - } } // MARK: API func fetchArticles(feeds: [Feed]) { + self.feeds = feeds + if feeds.count == 1 { nameForDisplay = feeds.first!.nameForDisplay } else { nameForDisplay = NSLocalizedString("Multiple", comment: "Multiple Feeds") } + + resetReadFilter() fetchAndReplaceArticlesAsync(feeds: feeds) } + func toggleReadFilter() { + guard let filter = isReadFiltered, let feedID = feeds.first?.feedID else { return } + readFilterEnabledTable[feedID] = !filter + isReadFiltered = !filter + rebuildTimelineItems(isReadFiltered: isReadFiltered) + } + func toggleReadStatusForSelectedArticles() { guard !selectedArticles.isEmpty else { return @@ -207,6 +215,24 @@ private extension TimelineModel { // MARK: Timeline Management + func resetReadFilter() { + guard feeds.count == 1, let timelineFeed = feeds.first else { + isReadFiltered = nil + return + } + + guard timelineFeed.defaultReadFilterType != .alwaysRead else { + isReadFiltered = nil + return + } + + if let feedID = timelineFeed.feedID, let readFilterEnabled = readFilterEnabledTable[feedID] { + isReadFiltered = readFilterEnabled + } else { + isReadFiltered = timelineFeed.defaultReadFilterType == .read + } + } + func sortParametersDidChange() { performBlockAndRestoreSelection { let unsortedArticles = Set(articles) @@ -253,7 +279,9 @@ private extension TimelineModel { // 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 fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: isReadFiltered, representedObjects: representedObjects) { [weak self] (articles, operation) in + 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 @@ -269,11 +297,12 @@ private extension TimelineModel { // TODO: Update unread counts and other item done in didSet on AppKit } - func rebuildTimelineItems(isReadFiltered: Bool) { + func rebuildTimelineItems(isReadFiltered: Bool?) { + let filtered = isReadFiltered ?? false let selectedArticleIDs = selectedArticles.map { $0.articleID } timelineItems = articles.compactMap { article in - if isReadFiltered && article.status.read && !selectedArticleIDs.contains(article.articleID) { + if filtered && article.status.read && !selectedArticleIDs.contains(article.articleID) { return nil } else { return TimelineItem(article: article) diff --git a/Multiplatform/Shared/Timeline/TimelineToolbarModifier.swift b/Multiplatform/Shared/Timeline/TimelineToolbarModifier.swift index 741480379..0d75b3bdf 100644 --- a/Multiplatform/Shared/Timeline/TimelineToolbarModifier.swift +++ b/Multiplatform/Shared/Timeline/TimelineToolbarModifier.swift @@ -19,15 +19,17 @@ struct TimelineToolbarModifier: ViewModifier { ToolbarItem(placement: .navigation) { Button (action: { withAnimation { - timelineModel.isReadFiltered.toggle() + timelineModel.toggleReadFilter() } }, label: { - if timelineModel.isReadFiltered { + if timelineModel.isReadFiltered ?? false { AppAssets.filterActiveImage.font(.title3) } else { AppAssets.filterInactiveImage.font(.title3) } - }).help(timelineModel.isReadFiltered ? "Show Read Articles" : "Filter Read Articles") + }) + .hidden(timelineModel.isReadFiltered == nil) + .help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles") } ToolbarItem { diff --git a/Multiplatform/Shared/Timeline/TimelineView.swift b/Multiplatform/Shared/Timeline/TimelineView.swift index d08de5580..7ea42bbc4 100644 --- a/Multiplatform/Shared/Timeline/TimelineView.swift +++ b/Multiplatform/Shared/Timeline/TimelineView.swift @@ -20,18 +20,19 @@ struct TimelineView: View { Spacer() Button (action: { withAnimation { - timelineModel.isReadFiltered.toggle() + timelineModel.toggleReadFilter() } }, label: { - if timelineModel.isReadFiltered { + if timelineModel.isReadFiltered ?? false { AppAssets.filterActiveImage } else { AppAssets.filterInactiveImage } }) + .hidden(timelineModel.isReadFiltered == nil) .padding(.top, 8).padding(.trailing) .buttonStyle(PlainButtonStyle()) - .help(timelineModel.isReadFiltered ? "Show Read Articles" : "Filter Read Articles") + .help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles") } ZStack { NavigationLink(destination: ArticleContainerView(articles: timelineModel.selectedArticles), isActive: $navigate) { diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 968be9eed..08d514817 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -296,6 +296,8 @@ 5193CD58245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; }; 5193CD59245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; }; 5193CD5A245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; }; + 5194736E24BBB937001A2939 /* HiddenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5194736D24BBB937001A2939 /* HiddenModifier.swift */; }; + 5194736F24BBB937001A2939 /* HiddenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5194736D24BBB937001A2939 /* HiddenModifier.swift */; }; 519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; }; 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E743422C663F900A78E47 /* SceneDelegate.swift */; }; 519ED456244828C3007F8E94 /* AddExtensionPointViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */; }; @@ -1960,6 +1962,7 @@ 51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = ""; }; 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = ""; }; 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RedditFeedProvider-Extensions.swift"; sourceTree = ""; }; + 5194736D24BBB937001A2939 /* HiddenModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenModifier.swift; sourceTree = ""; }; 519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = ""; }; 519E743422C663F900A78E47 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExtensionPointViewController.swift; sourceTree = ""; }; @@ -2687,6 +2690,7 @@ children = ( 514E6C0524AD2B5F00AC6F6E /* Image-Extensions.swift */, 5181C5AC24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift */, + 5194736D24BBB937001A2939 /* HiddenModifier.swift */, ); path = "SwiftUI Extensions"; sourceTree = ""; @@ -4992,6 +4996,7 @@ FF64D0E724AF53EE0084080A /* RefreshProgressModel.swift in Sources */, 51E4996A24A8762D00B667CB /* ExtractedArticle.swift in Sources */, 51919FF124AB864A00541E64 /* TimelineModel.swift in Sources */, + 5194736E24BBB937001A2939 /* HiddenModifier.swift in Sources */, 51E498F124A8085D00B667CB /* StarredFeedDelegate.swift in Sources */, 51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */, 51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */, @@ -5119,6 +5124,7 @@ 51E4993A24A8708800B667CB /* AppDelegate.swift in Sources */, 51E498CE24A8085D00B667CB /* UnreadFeed.swift in Sources */, 51E498C724A8085D00B667CB /* StarredFeedDelegate.swift in Sources */, + 5194736F24BBB937001A2939 /* HiddenModifier.swift in Sources */, 51919FB724AABCA100541E64 /* IconImageView.swift in Sources */, 51B54A6924B54A490014348B /* IconView.swift in Sources */, 51E498FA24A808BA00B667CB /* SingleFaviconDownloader.swift in Sources */,