From b8002e16cdff1bd4765fa98ed9760f73f2396ac6 Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 20 Mar 2026 16:37:17 +0800 Subject: [PATCH 1/5] Sync sidebar unread filtering with Hide Read Articles on Mac and iOS --- Mac/MainWindow/MainWindowController.swift | 10 +++++ .../Sidebar/SidebarViewController.swift | 40 ++++++++++++++----- iOS/SceneCoordinator.swift | 26 ++++++++---- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index d0affc9e6..89d21fb5b 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -560,6 +560,7 @@ final class MainWindowController: NSWindowController, NSUserInterfaceValidations @IBAction func toggleReadArticlesFilter(_ sender: Any?) { timelineContainerViewController?.toggleReadFilter() + syncSidebarReadArticlesFilter() } @objc func selectArticleTheme(_ menuItem: NSMenuItem) { @@ -604,6 +605,7 @@ extension MainWindowController: SidebarDelegate { timelineContainerViewController?.setRepresentedObjects(selectedObjects, mode: .regular) forceSearchToEnd() } + syncSidebarReadArticlesFilter() updateWindowTitle() NotificationCenter.default.post(name: .InspectableObjectsDidChange, object: nil) } @@ -662,6 +664,7 @@ extension MainWindowController: TimelineContainerViewControllerDelegate { } func timelineInvalidatedRestorationState(_: TimelineContainerViewController) { + syncSidebarReadArticlesFilter() invalidateRestorableState() } @@ -1045,6 +1048,7 @@ private extension MainWindowController { sidebarViewController?.restoreState(from: state.sidebarWindowState) timelineContainerViewController?.restoreState(from: state.timelineWindowState) + syncSidebarReadArticlesFilter() restoreArticleWindowScrollY = state.detailWindowState?.windowScrollY let isShowingExtractedArticle = state.detailWindowState?.isShowingExtractedArticle ?? false @@ -1071,6 +1075,7 @@ private extension MainWindowController { let articleWindowScrollY = state[UserInfoKey.articleWindowScrollY] as? CGFloat restoreArticleWindowScrollY = articleWindowScrollY timelineContainerViewController?.restoreLegacyState(from: state) + syncSidebarReadArticlesFilter() let isShowingExtractedArticle = state[UserInfoKey.isShowingExtractedArticle] as? Bool ?? false if isShowingExtractedArticle { @@ -1081,6 +1086,11 @@ private extension MainWindowController { // MARK: - Command Validation + func syncSidebarReadArticlesFilter() { + let isReadArticlesFiltered = timelineContainerViewController?.isReadFiltered ?? false + sidebarViewController?.setReadArticlesFilterEnabled(isReadArticlesFiltered) + } + func canCopyArticleURL() -> Bool { guard let selectedArticles else { return false diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index c7a762349..d99cbec20 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -45,12 +45,16 @@ extension Notification.Name { return SidebarOutlineDataSource(treeController: treeController) }() + private var isReadFeedsFiltered = false + private var isReadArticlesFiltered = false + var isReadFiltered: Bool { get { - return treeControllerDelegate.isReadFiltered + return isReadFeedsFiltered } set { - treeControllerDelegate.isReadFiltered = newValue + isReadFeedsFiltered = newValue + updateReadFilterState() } } var expandedTable = Set() @@ -184,7 +188,7 @@ extension Notification.Name { guard notification.object is AccountManager else { return } - if isReadFiltered { + if isReadFilterEnabled { rebuildTreeAndRestoreSelection() } } @@ -204,7 +208,7 @@ extension Notification.Name { return } - if isReadFiltered { + if isReadFilterEnabled { queueRebuildTreeAndRestoreSelection() } } @@ -491,7 +495,7 @@ extension Notification.Name { // MARK: - API func selectFeed(_ sidebarItem: SidebarItem) { - if isReadFiltered, let sidebarItemID = sidebarItem.sidebarItemID { + if isReadFilterEnabled, let sidebarItemID = sidebarItem.sidebarItemID { self.treeControllerDelegate.addFilterException(sidebarItemID) if let feed = sidebarItem as? Feed, let account = feed.account { @@ -518,15 +522,20 @@ extension Notification.Name { } func toggleReadFilter() { - if treeControllerDelegate.isReadFiltered { - isReadFiltered = false - } else { - isReadFiltered = true - } + isReadFiltered.toggle() delegate?.sidebarInvalidatedRestorationState(self) rebuildTreeAndRestoreSelection() } + func setReadArticlesFilterEnabled(_ isEnabled: Bool) { + guard isReadArticlesFiltered != isEnabled else { + return + } + isReadArticlesFiltered = isEnabled + updateReadFilterState() + rebuildTreeAndRestoreSelection() + } + } // MARK: - NSUserInterfaceValidations @@ -581,7 +590,7 @@ private extension SidebarViewController { } func addToFilterExceptionsIfNecessary(_ sidebarItem: SidebarItem?) { - if isReadFiltered, let sidebarItemID = sidebarItem?.sidebarItemID { + if isReadFilterEnabled, let sidebarItemID = sidebarItem?.sidebarItemID { if sidebarItem is PseudoFeed { treeControllerDelegate.addFilterException(sidebarItemID) } else if let folderFeed = sidebarItem as? Folder { @@ -629,6 +638,7 @@ private extension SidebarViewController { func rebuildTreeAndReloadDataIfNeeded() { if !animatingChanges && !BatchUpdate.shared.isPerforming { + updateReadFilterState() addAllSelectedToFilterExceptions() treeController.rebuild() treeControllerDelegate.resetFilterExceptions() @@ -637,6 +647,14 @@ private extension SidebarViewController { } } + var isReadFilterEnabled: Bool { + isReadFeedsFiltered || isReadArticlesFiltered + } + + func updateReadFilterState() { + treeControllerDelegate.isReadFiltered = isReadFilterEnabled + } + func expandNodes() { treeController.visitNodes(expandNodesVisitor(node:)) } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index feca7e731..c01ce81d5 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -131,6 +131,10 @@ struct SidebarItemNode: Hashable, Sendable { return hidingReadArticlesState.isHidingReadArticles(for: sidebarItemID) } + var isSidebarReadFiltered: Bool { + return isReadFeedsFiltered || isReadArticlesFiltered + } + var timelineDefaultReadFilterType: ReadFilterType { return timelineFeed?.defaultReadFilterType ?? .none } @@ -377,7 +381,8 @@ struct SidebarItemNode: Hashable, Sendable { // You can't assign the Feeds Read Filter until we've built the backing stores at least once or there is nothing // for state restoration to work with while we are waiting for the unread counts to initialize. - treeControllerDelegate.isReadFiltered = stateInfo.hideReadFeeds + AppDefaults.shared.hideReadFeeds = stateInfo.hideReadFeeds + updateSidebarReadFilterState() restoreSelectedSidebarItemAndArticle(stateInfo) } @@ -479,7 +484,7 @@ struct SidebarItemNode: Hashable, Sendable { return } - if isReadFeedsFiltered { + if isSidebarReadFiltered { rebuildBackingStores() } } @@ -742,7 +747,7 @@ struct SidebarItemNode: Hashable, Sendable { func cleanUp(conditional: Bool) { Self.logger.debug("SceneCoordinator: cleanUp: conditional \(conditional ? "true" : "false")") - if isReadFeedsFiltered { + if isSidebarReadFiltered { rebuildBackingStores() } if isReadArticlesFiltered && (AppDefaults.shared.refreshClearsReadArticles || !conditional) { @@ -754,8 +759,8 @@ struct SidebarItemNode: Hashable, Sendable { Self.logger.debug("SceneCoordinator: toggleReadFeedsFilter") let newValue = !isReadFeedsFiltered - treeControllerDelegate.isReadFiltered = newValue AppDefaults.shared.hideReadFeeds = newValue + updateSidebarReadFilterState() rebuildBackingStores() mainFeedCollectionViewController?.updateUI() } @@ -772,6 +777,8 @@ struct SidebarItemNode: Hashable, Sendable { return } hidingReadArticlesState.toggleHidingReadArticles(for: sidebarItemID) + updateSidebarReadFilterState() + rebuildBackingStores() refreshTimeline(resetScroll: false) } @@ -953,7 +960,7 @@ struct SidebarItemNode: Hashable, Sendable { self.activityManager.selecting(sidebarItem: sidebarItem) self.rootSplitViewController.show(.supplementary) setTimelineFeed(sidebarItem, animated: false) { - if self.isReadFeedsFiltered { + if self.isSidebarReadFiltered { self.rebuildBackingStores() } AppDefaults.shared.selectedSidebarItem = sidebarItem.sidebarItemID @@ -963,7 +970,7 @@ struct SidebarItemNode: Hashable, Sendable { } else { setTimelineFeed(nil, animated: false) { - if self.isReadFeedsFiltered { + if self.isSidebarReadFiltered { self.rebuildBackingStores() } self.activityManager.invalidateSelecting() @@ -1611,7 +1618,7 @@ private extension SceneCoordinator { } func addToFilterExceptionsIfNecessary(_ sidebarItem: SidebarItem?) { - if isReadFeedsFiltered, let sidebarItemID = sidebarItem?.sidebarItemID { + if isSidebarReadFiltered, let sidebarItemID = sidebarItem?.sidebarItemID { if sidebarItem is SmartFeed { treeControllerDelegate.addFilterException(sidebarItemID) } else if let folderFeed = sidebarItem as? Folder { @@ -1668,6 +1675,7 @@ private extension SceneCoordinator { Self.rebuildCount += 1 #endif + updateSidebarReadFilterState() addToFilterExceptionsIfNecessary(timelineFeed) treeController.rebuild() treeControllerDelegate.resetFilterExceptions() @@ -1765,6 +1773,10 @@ private extension SceneCoordinator { } } + func updateSidebarReadFilterState() { + treeControllerDelegate.isReadFiltered = isSidebarReadFiltered + } + func updateShowNamesAndIcons() { if timelineFeed is Feed { From 79f06cef228c86b242c946aeb4526fed64545e37 Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 20 Mar 2026 17:16:49 +0800 Subject: [PATCH 2/5] revert ios --- iOS/SceneCoordinator.swift | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index c01ce81d5..feca7e731 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -131,10 +131,6 @@ struct SidebarItemNode: Hashable, Sendable { return hidingReadArticlesState.isHidingReadArticles(for: sidebarItemID) } - var isSidebarReadFiltered: Bool { - return isReadFeedsFiltered || isReadArticlesFiltered - } - var timelineDefaultReadFilterType: ReadFilterType { return timelineFeed?.defaultReadFilterType ?? .none } @@ -381,8 +377,7 @@ struct SidebarItemNode: Hashable, Sendable { // You can't assign the Feeds Read Filter until we've built the backing stores at least once or there is nothing // for state restoration to work with while we are waiting for the unread counts to initialize. - AppDefaults.shared.hideReadFeeds = stateInfo.hideReadFeeds - updateSidebarReadFilterState() + treeControllerDelegate.isReadFiltered = stateInfo.hideReadFeeds restoreSelectedSidebarItemAndArticle(stateInfo) } @@ -484,7 +479,7 @@ struct SidebarItemNode: Hashable, Sendable { return } - if isSidebarReadFiltered { + if isReadFeedsFiltered { rebuildBackingStores() } } @@ -747,7 +742,7 @@ struct SidebarItemNode: Hashable, Sendable { func cleanUp(conditional: Bool) { Self.logger.debug("SceneCoordinator: cleanUp: conditional \(conditional ? "true" : "false")") - if isSidebarReadFiltered { + if isReadFeedsFiltered { rebuildBackingStores() } if isReadArticlesFiltered && (AppDefaults.shared.refreshClearsReadArticles || !conditional) { @@ -759,8 +754,8 @@ struct SidebarItemNode: Hashable, Sendable { Self.logger.debug("SceneCoordinator: toggleReadFeedsFilter") let newValue = !isReadFeedsFiltered + treeControllerDelegate.isReadFiltered = newValue AppDefaults.shared.hideReadFeeds = newValue - updateSidebarReadFilterState() rebuildBackingStores() mainFeedCollectionViewController?.updateUI() } @@ -777,8 +772,6 @@ struct SidebarItemNode: Hashable, Sendable { return } hidingReadArticlesState.toggleHidingReadArticles(for: sidebarItemID) - updateSidebarReadFilterState() - rebuildBackingStores() refreshTimeline(resetScroll: false) } @@ -960,7 +953,7 @@ struct SidebarItemNode: Hashable, Sendable { self.activityManager.selecting(sidebarItem: sidebarItem) self.rootSplitViewController.show(.supplementary) setTimelineFeed(sidebarItem, animated: false) { - if self.isSidebarReadFiltered { + if self.isReadFeedsFiltered { self.rebuildBackingStores() } AppDefaults.shared.selectedSidebarItem = sidebarItem.sidebarItemID @@ -970,7 +963,7 @@ struct SidebarItemNode: Hashable, Sendable { } else { setTimelineFeed(nil, animated: false) { - if self.isSidebarReadFiltered { + if self.isReadFeedsFiltered { self.rebuildBackingStores() } self.activityManager.invalidateSelecting() @@ -1618,7 +1611,7 @@ private extension SceneCoordinator { } func addToFilterExceptionsIfNecessary(_ sidebarItem: SidebarItem?) { - if isSidebarReadFiltered, let sidebarItemID = sidebarItem?.sidebarItemID { + if isReadFeedsFiltered, let sidebarItemID = sidebarItem?.sidebarItemID { if sidebarItem is SmartFeed { treeControllerDelegate.addFilterException(sidebarItemID) } else if let folderFeed = sidebarItem as? Folder { @@ -1675,7 +1668,6 @@ private extension SceneCoordinator { Self.rebuildCount += 1 #endif - updateSidebarReadFilterState() addToFilterExceptionsIfNecessary(timelineFeed) treeController.rebuild() treeControllerDelegate.resetFilterExceptions() @@ -1773,10 +1765,6 @@ private extension SceneCoordinator { } } - func updateSidebarReadFilterState() { - treeControllerDelegate.isReadFiltered = isSidebarReadFiltered - } - func updateShowNamesAndIcons() { if timelineFeed is Feed { From 12297b5faff884b236084b54a8d18dc69dd29a8d Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 20 Mar 2026 20:25:23 +0800 Subject: [PATCH 3/5] Stabilize global hide-read behavior and reduce Swift type-check overhead in Mac UI controllers --- Mac/AppDefaults.swift | 17 ++++ .../AddFolder/AddFolderWindowController.swift | 33 ++++--- Mac/MainWindow/MainWindowController.swift | 4 +- .../OPML/ImportOPMLWindowController.swift | 13 +-- .../Timeline/TimelineViewController.swift | 87 ++++++------------- .../GeneralPrefencesViewController.swift | 20 +++-- 6 files changed, 87 insertions(+), 87 deletions(-) diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift index 51985e00d..9adc66375 100644 --- a/Mac/AppDefaults.swift +++ b/Mac/AppDefaults.swift @@ -30,6 +30,7 @@ final class AppDefaults: Sendable { static let timelineFontSize = "timelineFontSize" static let timelineSortDirection = "timelineSortDirection" static let timelineGroupByFeed = "timelineGroupByFeed" + static let timelineReadFilterEnabled = "timelineReadFilterEnabled" static let detailFontSize = "detailFontSize" static let openInBrowserInBackground = "openInBrowserInBackground" static let subscribeToFeedsInDefaultBrowser = "subscribeToFeedsInDefaultBrowser" @@ -149,6 +150,22 @@ final class AppDefaults: Sendable { } } + var timelineReadFilterEnabled: Bool? { + get { + guard UserDefaults.standard.object(forKey: Key.timelineReadFilterEnabled) != nil else { + return nil + } + return UserDefaults.standard.bool(forKey: Key.timelineReadFilterEnabled) + } + set { + if let newValue { + UserDefaults.standard.set(newValue, forKey: Key.timelineReadFilterEnabled) + } else { + UserDefaults.standard.removeObject(forKey: Key.timelineReadFilterEnabled) + } + } + } + var detailFontSize: FontSize { get { return fontSize(for: Key.detailFontSize) diff --git a/Mac/MainWindow/AddFolder/AddFolderWindowController.swift b/Mac/MainWindow/AddFolder/AddFolderWindowController.swift index 0e9c1626d..ca44c77b1 100644 --- a/Mac/MainWindow/AddFolder/AddFolderWindowController.swift +++ b/Mac/MainWindow/AddFolder/AddFolderWindowController.swift @@ -40,24 +40,16 @@ final class AddFolderWindowController: NSWindowController { // MARK: - NSViewController override func windowDidLoad() { - let preferredAccountID = AppDefaults.shared.addFolderAccountID accountPopupButton.removeAllItems() let menu = NSMenu() accountPopupButton.menu = menu - let sortedAccounts = AccountManager.shared.sortedActiveAccounts - let accounts: [Account] = sortedAccounts.filter { !$0.behaviors.contains(.disallowFolderManagement) } - - for oneAccount in accounts { - - let oneMenuItem = NSMenuItem() - oneMenuItem.title = oneAccount.nameForDisplay - oneMenuItem.representedObject = oneAccount - menu.addItem(oneMenuItem) - - if oneAccount.accountID == preferredAccountID { - accountPopupButton.select(oneMenuItem) + let preferredAccountID = AppDefaults.shared.addFolderAccountID + for account in allowedAccountsForFolderCreation() { + let menuItem = addMenuItem(to: menu, for: account) + if account.accountID == preferredAccountID { + accountPopupButton.select(menuItem) } } } @@ -98,6 +90,21 @@ extension AddFolderWindowController: NSTextFieldDelegate { private extension AddFolderWindowController { + func allowedAccountsForFolderCreation() -> [Account] { + AccountManager.shared.sortedActiveAccounts.filter { account in + !account.behaviors.contains(.disallowFolderManagement) + } + } + + @discardableResult + func addMenuItem(to menu: NSMenu, for account: Account) -> NSMenuItem { + let menuItem = NSMenuItem() + menuItem.title = account.nameForDisplay + menuItem.representedObject = account + menu.addItem(menuItem) + return menuItem + } + private func addFolderIfNeeded() { guard let menuItem = accountPopupButton.selectedItem else { return diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index 89d21fb5b..7269fa5ee 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -605,7 +605,6 @@ extension MainWindowController: SidebarDelegate { timelineContainerViewController?.setRepresentedObjects(selectedObjects, mode: .regular) forceSearchToEnd() } - syncSidebarReadArticlesFilter() updateWindowTitle() NotificationCenter.default.post(name: .InspectableObjectsDidChange, object: nil) } @@ -664,7 +663,6 @@ extension MainWindowController: TimelineContainerViewControllerDelegate { } func timelineInvalidatedRestorationState(_: TimelineContainerViewController) { - syncSidebarReadArticlesFilter() invalidateRestorableState() } @@ -1087,7 +1085,7 @@ private extension MainWindowController { // MARK: - Command Validation func syncSidebarReadArticlesFilter() { - let isReadArticlesFiltered = timelineContainerViewController?.isReadFiltered ?? false + let isReadArticlesFiltered = AppDefaults.shared.timelineReadFilterEnabled ?? false sidebarViewController?.setReadArticlesFilterEnabled(isReadArticlesFiltered) } diff --git a/Mac/MainWindow/OPML/ImportOPMLWindowController.swift b/Mac/MainWindow/OPML/ImportOPMLWindowController.swift index 7f68d6ab3..ddc985db5 100644 --- a/Mac/MainWindow/OPML/ImportOPMLWindowController.swift +++ b/Mac/MainWindow/OPML/ImportOPMLWindowController.swift @@ -32,19 +32,20 @@ final class ImportOPMLWindowController: NSWindowController { if oneAccount.behaviors.contains(.disallowOPMLImports) { continue } - addMenuItem(to: menu, for: oneAccount, savedAccountID: savedAccountID) + let menuItem = addMenuItem(to: menu, for: oneAccount) + if oneAccount.accountID == savedAccountID { + accountPopUpButton.select(menuItem) + } } } - private func addMenuItem(to menu: NSMenu, for account: Account, savedAccountID: String?) { + @discardableResult + private func addMenuItem(to menu: NSMenu, for account: Account) -> NSMenuItem { let menuItem = NSMenuItem() menuItem.title = account.nameForDisplay menuItem.representedObject = account menu.addItem(menuItem) - - if account.accountID == savedAccountID { - accountPopUpButton.select(menuItem) - } + return menuItem } // MARK: API diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 2c57dc299..2c6832602 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -30,7 +30,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr var sharingServicePickerDelegate: NSSharingServicePickerDelegate? - private var readFilterEnabledTable = [SidebarItemIdentifier: Bool]() + private var globalReadFilterEnabled = AppDefaults.shared.timelineReadFilterEnabled var isReadFiltered: Bool? { guard representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? SidebarItem else { @@ -39,8 +39,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr guard timelineFeed.defaultReadFilterType != .alwaysRead else { return nil } - if let sidebarItemID = timelineFeed.sidebarItemID, let readFiltered = readFilterEnabledTable[sidebarItemID] { - return readFiltered + if let globalReadFilterEnabled { + return globalReadFilterEnabled } return timelineFeed.defaultReadFilterType == .read } @@ -66,7 +66,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr var representedObjects: [AnyObject]? { didSet { if !representedObjectArraysAreEqual(oldValue, representedObjects) { - seedReadFilterForFolders() unreadCount = 0 selectionDidChange(nil) @@ -84,18 +83,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr } var windowState: TimelineWindowState { - let readArticlesFilterStateKeys = readFilterEnabledTable.keys.compactMap { $0.userInfo } - let readArticlesFilterStateValues = readFilterEnabledTable.values.compactMap( { $0 }) - if selectedArticles.count == 1 { let path = selectedArticles.first!.pathUserInfo - return TimelineWindowState(readArticlesFilterStateKeys: readArticlesFilterStateKeys, - readArticlesFilterStateValues: readArticlesFilterStateValues, + return TimelineWindowState(readArticlesFilterStateKeys: [], + readArticlesFilterStateValues: [], selectedAccountID: path[ArticlePathKey.accountID] as? String, selectedArticleID: path[ArticlePathKey.articleID] as? String) } else { - return TimelineWindowState(readArticlesFilterStateKeys: readArticlesFilterStateKeys, - readArticlesFilterStateValues: readArticlesFilterStateValues, + return TimelineWindowState(readArticlesFilterStateKeys: [], + readArticlesFilterStateValues: [], selectedAccountID: nil, selectedArticleID: nil) } @@ -290,41 +286,19 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr } func toggleReadFilter() { - guard let filter = isReadFiltered, - let sidebarItemID = (representedObjects?.first as? SidebarItem)?.sidebarItemID else { + guard let filter = isReadFiltered else { return } - if filter { - noteSidebarItemShowsReadArticles(sidebarItemID) - } else { - noteSidebarItemHidesReadArticles(sidebarItemID) - } + let newValue = !filter + globalReadFilterEnabled = newValue + AppDefaults.shared.timelineReadFilterEnabled = newValue delegate?.timelineInvalidatedRestorationState(self) fetchAndReplacePreservingSelection() } // MARK: State Restoration - private func noteSidebarItemHidesReadArticles(_ sidebarItemID: SidebarItemIdentifier) { - readFilterEnabledTable[sidebarItemID] = true - } - - private func noteSidebarItemShowsReadArticles(_ sidebarItemID: SidebarItemIdentifier) { - readFilterEnabledTable[sidebarItemID] = false - } - func restoreState(from state: TimelineWindowState) { - for i in 0..() } + let readFilterEnabledTable = readFilterTableForCurrentFetchers(fetchers) var fetchedArticles = Set
() for fetchers in fetchers { @@ -1245,6 +1210,7 @@ private extension TimelineViewController { precondition(Thread.isMainThread) cancelPendingAsyncFetches() let fetchers = representedObjects.compactMap { $0 as? ArticleFetcher } + let readFilterEnabledTable = readFilterTableForCurrentFetchers(fetchers) let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilterEnabledTable: readFilterEnabledTable, fetchers: fetchers) { [weak self] (articles, operation) in precondition(Thread.isMainThread) guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { @@ -1301,18 +1267,21 @@ private extension TimelineViewController { return representedObjects?.contains(where: { $0 is Folder }) ?? false } - func seedReadFilterForFolders() { - guard let representedObjects else { - return + func readFilterTableForCurrentFetchers(_ fetchers: [ArticleFetcher]) -> [SidebarItemIdentifier: Bool] { + guard let globalReadFilterEnabled else { + return [:] } - for object in representedObjects { - guard let folder = object as? Folder, let sidebarItemID = folder.sidebarItemID else { + + var table = [SidebarItemIdentifier: Bool]() + for fetcher in fetchers { + guard let sidebarItem = fetcher as? SidebarItem, + sidebarItem.defaultReadFilterType != .alwaysRead, + let sidebarItemID = sidebarItem.sidebarItemID else { continue } - if readFilterEnabledTable[sidebarItemID] == nil { - readFilterEnabledTable[sidebarItemID] = true - } + table[sidebarItemID] = globalReadFilterEnabled } + return table } func representedObjectsContainsAnyFeed(_ feeds: Set) -> Bool { diff --git a/Mac/Preferences/General/GeneralPrefencesViewController.swift b/Mac/Preferences/General/GeneralPrefencesViewController.swift index 2b906dd2d..f0ffa75e9 100644 --- a/Mac/Preferences/General/GeneralPrefencesViewController.swift +++ b/Mac/Preferences/General/GeneralPrefencesViewController.swift @@ -52,12 +52,8 @@ final class GeneralPreferencesViewController: NSViewController { NSWorkspace.shared.open(url) } - @IBAction func articleThemePopUpDidChange(_ sender: Any) { - guard let menuItem = articleThemePopup.selectedItem else { - return - } - ArticleThemesManager.shared.currentThemeName = menuItem.title - updateArticleThemePopup() + @IBAction func articleThemePopUpDidChange(_ sender: Any?) { + applySelectedArticleTheme() } @IBAction func browserPopUpDidChangeValue(_ sender: Any?) { @@ -133,6 +129,18 @@ private extension GeneralPreferencesViewController { } } + func applySelectedArticleTheme() { + let selectedThemeName: String + if let selectedItem = articleThemePopup.selectedItem { + selectedThemeName = selectedItem.title + } else { + selectedThemeName = ArticleTheme.defaultTheme.name + } + + ArticleThemesManager.shared.currentThemeName = selectedThemeName + updateArticleThemePopup() + } + func updateBrowserPopup() { let menu = defaultBrowserPopup.menu! let allBrowsers = MacWebBrowser.sortedBrowsers() From c40c2915a77fd36847ee40ae7c121babf2863231 Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 20 Mar 2026 20:32:31 +0800 Subject: [PATCH 4/5] Make the same changes to iOS. --- iOS/AppDefaults.swift | 20 ++++- iOS/HidingReadArticlesState.swift | 118 +++--------------------------- 2 files changed, 29 insertions(+), 109 deletions(-) diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index 63a1a28ab..84286a5fa 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -67,6 +67,7 @@ final class AppDefaults: Sendable { static let currentThemeName = "currentThemeName" static let articleContentJavascriptEnabled = "articleContentJavascriptEnabled" static let hideReadFeeds = "hideReadFeeds" + static let hideReadArticles = "hideReadArticles" static let isShowingExtractedArticle = "isShowingExtractedArticle" static let articleWindowScrollY = "articleWindowScrollY" static let expandedContainers = "expandedContainers" @@ -276,6 +277,15 @@ final class AppDefaults: Sendable { } } + var hideReadArticles: Bool { + get { + UserDefaults.standard.bool(forKey: Key.hideReadArticles) + } + set { + UserDefaults.standard.set(newValue, forKey: Key.hideReadArticles) + } + } + var isShowingExtractedArticle: Bool { get { UserDefaults.standard.bool(forKey: Key.isShowingExtractedArticle) @@ -466,6 +476,7 @@ private extension AppDefaults { struct StateRestorationInfo { let hideReadFeeds: Bool + let hideReadArticles: Bool let expandedContainers: Set let selectedSidebarItem: SidebarItemIdentifier? let smartFeedsHidingReadArticles: Set @@ -476,6 +487,7 @@ struct StateRestorationInfo { let isShowingExtractedArticle: Bool init(hideReadFeeds: Bool, + hideReadArticles: Bool, expandedContainers: Set, selectedSidebarItem: SidebarItemIdentifier?, smartFeedsHidingReadArticles: Set, @@ -485,6 +497,7 @@ struct StateRestorationInfo { articleWindowScrollY: Int, isShowingExtractedArticle: Bool) { self.hideReadFeeds = hideReadFeeds + self.hideReadArticles = hideReadArticles self.expandedContainers = expandedContainers self.selectedSidebarItem = selectedSidebarItem self.smartFeedsHidingReadArticles = smartFeedsHidingReadArticles @@ -494,11 +507,12 @@ struct StateRestorationInfo { self.articleWindowScrollY = articleWindowScrollY self.isShowingExtractedArticle = isShowingExtractedArticle - AppDefaults.logger.debug("AppDefaults: StateRestorationInfo:\nexpandedContainers: \(expandedContainers)\nselectedSidebarItem: \(selectedSidebarItem?.userInfo ?? [String: String]())\nsmartFeedsHidingReadArticles: \(smartFeedsHidingReadArticles)\nfeedsHidingReadArticles: \(feedsHidingReadArticles)\nfoldersShowingReadArticles: \(foldersShowingReadArticles)\nselectedArticle: \(selectedArticle?.dictionary ?? [String: String]())\narticleWindowScrollY: \(articleWindowScrollY)\nisShowingExtractedArticle: \(isShowingExtractedArticle ? "true" : "false")") + AppDefaults.logger.debug("AppDefaults: StateRestorationInfo:\nhideReadArticles: \(hideReadArticles ? "true" : "false")\nexpandedContainers: \(expandedContainers)\nselectedSidebarItem: \(selectedSidebarItem?.userInfo ?? [String: String]())\nsmartFeedsHidingReadArticles: \(smartFeedsHidingReadArticles)\nfeedsHidingReadArticles: \(feedsHidingReadArticles)\nfoldersShowingReadArticles: \(foldersShowingReadArticles)\nselectedArticle: \(selectedArticle?.dictionary ?? [String: String]())\narticleWindowScrollY: \(articleWindowScrollY)\nisShowingExtractedArticle: \(isShowingExtractedArticle ? "true" : "false")") } init() { self.init(hideReadFeeds: AppDefaults.shared.hideReadFeeds, + hideReadArticles: AppDefaults.shared.hideReadArticles, expandedContainers: AppDefaults.shared.expandedContainers, selectedSidebarItem: AppDefaults.shared.selectedSidebarItem, smartFeedsHidingReadArticles: AppDefaults.shared.smartFeedsHidingReadArticles, @@ -568,14 +582,17 @@ struct StateRestorationInfo { var smartFeedsHidingReadArticles = Set() var feedsHidingReadArticles = [String: Set]() + var hideReadArticles = AppDefaults.shared.hideReadArticles for sidebarItem in sidebarItemsHidingReadArticles { switch sidebarItem { case .smartFeed(let id): smartFeedsHidingReadArticles.insert(id) + hideReadArticles = true case .feed(let accountID, let feedID): var feedIDs = feedsHidingReadArticles[accountID] ?? Set() feedIDs.insert(feedID) feedsHidingReadArticles[accountID] = feedIDs + hideReadArticles = true default: continue } @@ -590,6 +607,7 @@ struct StateRestorationInfo { } self.init(hideReadFeeds: hideReadFeeds, + hideReadArticles: hideReadArticles, expandedContainers: expandedContainers, selectedSidebarItem: selectedSidebarItem, smartFeedsHidingReadArticles: smartFeedsHidingReadArticles, diff --git a/iOS/HidingReadArticlesState.swift b/iOS/HidingReadArticlesState.swift index 67a0b1aad..324514c0e 100644 --- a/iOS/HidingReadArticlesState.swift +++ b/iOS/HidingReadArticlesState.swift @@ -10,20 +10,18 @@ import Foundation import Account @MainActor final class HidingReadArticlesState { - private var smartFeedsHidingReadArticles = Set() - private var feedsHidingReadArticles = [String: Set]() // accountID: Set - private var foldersShowingReadArticles = [String: Set]() // accountID: Set + private var hideReadArticles = AppDefaults.shared.hideReadArticles func copy(from stateRestorationInfo: StateRestorationInfo) { - smartFeedsHidingReadArticles = stateRestorationInfo.smartFeedsHidingReadArticles - feedsHidingReadArticles = stateRestorationInfo.feedsHidingReadArticles - foldersShowingReadArticles = stateRestorationInfo.foldersShowingReadArticles + hideReadArticles = stateRestorationInfo.hideReadArticles } func save() { - saveSmartFeedsHidingReadArticles() - saveFeedsHidingReadArticles() - saveFoldersShowingReadArticles() + AppDefaults.shared.hideReadArticles = hideReadArticles + // Clear per-sidebar-item legacy state now that read filtering is global. + AppDefaults.shared.smartFeedsHidingReadArticles = [] + AppDefaults.shared.feedsHidingReadArticles = [:] + AppDefaults.shared.foldersShowingReadArticles = [:] } func toggleHidingReadArticles(for sidebarItemID: SidebarItemIdentifier) { @@ -32,35 +30,12 @@ import Account return } - let hidesReadArticles = isHidingReadArticles(for: sidebarItemID) - let toggledValue = !hidesReadArticles - saveHidingReadArticles(for: sidebarItemID, hiding: toggledValue) + hideReadArticles.toggle() + save() } func isHidingReadArticles(for sidebarItemID: SidebarItemIdentifier) -> Bool { - switch sidebarItemID { - - case .smartFeed(let id): - if isUnreadSmartFeed(sidebarItemID) { - return true - } - return smartFeedsHidingReadArticles.contains(id) - - case .feed(let accountID, let feedID): - var isHidingReadArticles = false - if let feedIDs = feedsHidingReadArticles[accountID] { - isHidingReadArticles = feedIDs.contains(feedID) - } - return isHidingReadArticles - - case .folder(let accountID, let folderName): - // Folders hide read articles by default, so we check if not showing read articles. - var isHidingReadArticles = true - if let folderNames = foldersShowingReadArticles[accountID] { - isHidingReadArticles = !folderNames.contains(folderName) - } - return isHidingReadArticles - } + isUnreadSmartFeed(sidebarItemID) ? true : hideReadArticles } func canToggleHidingReadArticles(for sidebarItemID: SidebarItemIdentifier) -> Bool { @@ -74,77 +49,4 @@ private extension HidingReadArticlesState { func isUnreadSmartFeed(_ sidebarItemID: SidebarItemIdentifier) -> Bool { sidebarItemID == SmartFeedsController.shared.unreadFeed.sidebarItemID } - - func saveHidingReadArticles(for sidebarItemID: SidebarItemIdentifier, hiding: Bool) { - switch sidebarItemID { - - case .smartFeed(let id): - if isUnreadSmartFeed(sidebarItemID) { - return - } - if hiding { - smartFeedsHidingReadArticles.insert(id) - } else { - smartFeedsHidingReadArticles.remove(id) - } - saveSmartFeedsHidingReadArticles() - - case .feed(let accountID, let feedID): - if hiding { - var feedIDs = feedsHidingReadArticles[accountID] ?? Set() - feedIDs.insert(feedID) - feedsHidingReadArticles[accountID] = feedIDs - } else { - feedsHidingReadArticles[accountID]?.remove(feedID) - } - saveFeedsHidingReadArticles() - - case .folder(let accountID, let folderName): - // Folders hide read articles by default, so we store the folder - // only if it's showing read articles. It's the opposite of - // feedsHidingReadArticles. - if hiding { - foldersShowingReadArticles[accountID]?.remove(folderName) - } else { - var folderNames = foldersShowingReadArticles[accountID] ?? Set() - folderNames.insert(folderName) - foldersShowingReadArticles[accountID] = folderNames - } - saveFoldersShowingReadArticles() - } - } - - func saveFoldersShowingReadArticles() { - var d = foldersShowingReadArticles - - // Filter out accounts and folders that no longer exist. - for accountID in Array(d.keys) { - guard let account = AccountManager.shared.existingAccount(accountID: accountID) else { - d[accountID] = nil - continue - } - d[accountID] = d[accountID]?.filter { account.existingFolder(withDisplayName: $0) != nil } - } - - AppDefaults.shared.foldersShowingReadArticles = d - } - - func saveFeedsHidingReadArticles() { - var d = feedsHidingReadArticles - - // Filter out accounts and feeds that no longer exist. - for accountID in Array(d.keys) { - guard let account = AccountManager.shared.existingAccount(accountID: accountID) else { - d[accountID] = nil - continue - } - d[accountID] = d[accountID]?.filter { account.existingFeed(withFeedID: $0) != nil } - } - - AppDefaults.shared.feedsHidingReadArticles = d - } - - func saveSmartFeedsHidingReadArticles() { - AppDefaults.shared.smartFeedsHidingReadArticles = smartFeedsHidingReadArticles - } } From 94a85cfcae4253719a2f9539aa291f24f9b68629 Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 20 Mar 2026 20:54:53 +0800 Subject: [PATCH 5/5] Unify read filtering into one global state and fix sidebar refresh regression --- Mac/AppDefaults.swift | 11 ++---- Mac/MainWindow/MainWindowController.swift | 36 +++++++++---------- .../Sidebar/SidebarViewController.swift | 35 +++++++----------- .../TimelineContainerViewController.swift | 5 +++ .../Timeline/TimelineViewController.swift | 23 ++++++------ iOS/AppDefaults.swift | 20 +---------- iOS/HidingReadArticlesState.swift | 11 +++--- iOS/SceneCoordinator.swift | 5 +++ 8 files changed, 60 insertions(+), 86 deletions(-) diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift index 9adc66375..e14b68aba 100644 --- a/Mac/AppDefaults.swift +++ b/Mac/AppDefaults.swift @@ -150,19 +150,12 @@ final class AppDefaults: Sendable { } } - var timelineReadFilterEnabled: Bool? { + var timelineReadFilterEnabled: Bool { get { - guard UserDefaults.standard.object(forKey: Key.timelineReadFilterEnabled) != nil else { - return nil - } return UserDefaults.standard.bool(forKey: Key.timelineReadFilterEnabled) } set { - if let newValue { - UserDefaults.standard.set(newValue, forKey: Key.timelineReadFilterEnabled) - } else { - UserDefaults.standard.removeObject(forKey: Key.timelineReadFilterEnabled) - } + UserDefaults.standard.set(newValue, forKey: Key.timelineReadFilterEnabled) } } diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index 7269fa5ee..77afc67ef 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -555,12 +555,11 @@ final class MainWindowController: NSWindowController, NSUserInterfaceValidations } @IBAction func toggleReadFeedsFilter(_ sender: Any?) { - sidebarViewController?.toggleReadFilter() + toggleGlobalReadFilter() } @IBAction func toggleReadArticlesFilter(_ sender: Any?) { - timelineContainerViewController?.toggleReadFilter() - syncSidebarReadArticlesFilter() + toggleGlobalReadFilter() } @objc func selectArticleTheme(_ menuItem: NSMenuItem) { @@ -1046,7 +1045,7 @@ private extension MainWindowController { sidebarViewController?.restoreState(from: state.sidebarWindowState) timelineContainerViewController?.restoreState(from: state.timelineWindowState) - syncSidebarReadArticlesFilter() + syncSidebarReadFilter() restoreArticleWindowScrollY = state.detailWindowState?.windowScrollY let isShowingExtractedArticle = state.detailWindowState?.isShowingExtractedArticle ?? false @@ -1073,7 +1072,7 @@ private extension MainWindowController { let articleWindowScrollY = state[UserInfoKey.articleWindowScrollY] as? CGFloat restoreArticleWindowScrollY = articleWindowScrollY timelineContainerViewController?.restoreLegacyState(from: state) - syncSidebarReadArticlesFilter() + syncSidebarReadFilter() let isShowingExtractedArticle = state[UserInfoKey.isShowingExtractedArticle] as? Bool ?? false if isShowingExtractedArticle { @@ -1084,9 +1083,18 @@ private extension MainWindowController { // MARK: - Command Validation - func syncSidebarReadArticlesFilter() { - let isReadArticlesFiltered = AppDefaults.shared.timelineReadFilterEnabled ?? false - sidebarViewController?.setReadArticlesFilterEnabled(isReadArticlesFiltered) + func syncSidebarReadFilter() { + let isReadFiltered = AppDefaults.shared.timelineReadFilterEnabled + sidebarViewController?.setReadFilterEnabled(isReadFiltered) + timelineContainerViewController?.setReadFilterEnabled(isReadFiltered) + } + + func toggleGlobalReadFilter() { + let newValue = !AppDefaults.shared.timelineReadFilterEnabled + AppDefaults.shared.timelineReadFilterEnabled = newValue + syncSidebarReadFilter() + makeToolbarValidate() + invalidateRestorableState() } func canCopyArticleURL() -> Bool { @@ -1257,22 +1265,14 @@ private extension MainWindowController { let showCommand = NSLocalizedString("Show Read Feeds", comment: "Command") let hideCommand = NSLocalizedString("Hide Read Feeds", comment: "Command") - menuItem.title = sidebarViewController?.isReadFiltered ?? false ? showCommand : hideCommand + menuItem.title = AppDefaults.shared.timelineReadFilterEnabled ? showCommand : hideCommand return true } func validateToggleReadArticles(_ item: NSValidatedUserInterfaceItem) -> Bool { let showCommand = NSLocalizedString("Show Read Articles", comment: "Command") let hideCommand = NSLocalizedString("Hide Read Articles", comment: "Command") - - guard let isReadFiltered = timelineContainerViewController?.isReadFiltered else { - (item as? NSMenuItem)?.title = hideCommand - if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton { - toolbarItem.toolTip = hideCommand - button.image = Assets.Images.filterInactive - } - return false - } + let isReadFiltered = AppDefaults.shared.timelineReadFilterEnabled if isReadFiltered { (item as? NSMenuItem)?.title = showCommand diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index d99cbec20..2fabc1d65 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -45,16 +45,13 @@ extension Notification.Name { return SidebarOutlineDataSource(treeController: treeController) }() - private var isReadFeedsFiltered = false - private var isReadArticlesFiltered = false - var isReadFiltered: Bool { get { - return isReadFeedsFiltered + return AppDefaults.shared.timelineReadFilterEnabled } set { - isReadFeedsFiltered = newValue - updateReadFilterState() + AppDefaults.shared.timelineReadFilterEnabled = newValue + treeControllerDelegate.isReadFiltered = newValue } } var expandedTable = Set() @@ -73,6 +70,7 @@ extension Notification.Name { // MARK: - NSViewController override func viewDidLoad() { + treeControllerDelegate.isReadFiltered = isReadFiltered outlineView.dataSource = dataSource outlineView.doubleAction = #selector(doubleClickedSidebar(_:)) outlineView.setDraggingSourceOperationMask([.move, .copy], forLocal: true) @@ -188,7 +186,7 @@ extension Notification.Name { guard notification.object is AccountManager else { return } - if isReadFilterEnabled { + if isReadFiltered { rebuildTreeAndRestoreSelection() } } @@ -208,7 +206,7 @@ extension Notification.Name { return } - if isReadFilterEnabled { + if isReadFiltered { queueRebuildTreeAndRestoreSelection() } } @@ -495,7 +493,7 @@ extension Notification.Name { // MARK: - API func selectFeed(_ sidebarItem: SidebarItem) { - if isReadFilterEnabled, let sidebarItemID = sidebarItem.sidebarItemID { + if isReadFiltered, let sidebarItemID = sidebarItem.sidebarItemID { self.treeControllerDelegate.addFilterException(sidebarItemID) if let feed = sidebarItem as? Feed, let account = feed.account { @@ -527,12 +525,11 @@ extension Notification.Name { rebuildTreeAndRestoreSelection() } - func setReadArticlesFilterEnabled(_ isEnabled: Bool) { - guard isReadArticlesFiltered != isEnabled else { + func setReadFilterEnabled(_ isEnabled: Bool) { + guard treeControllerDelegate.isReadFiltered != isEnabled else { return } - isReadArticlesFiltered = isEnabled - updateReadFilterState() + isReadFiltered = isEnabled rebuildTreeAndRestoreSelection() } @@ -590,7 +587,7 @@ private extension SidebarViewController { } func addToFilterExceptionsIfNecessary(_ sidebarItem: SidebarItem?) { - if isReadFilterEnabled, let sidebarItemID = sidebarItem?.sidebarItemID { + if isReadFiltered, let sidebarItemID = sidebarItem?.sidebarItemID { if sidebarItem is PseudoFeed { treeControllerDelegate.addFilterException(sidebarItemID) } else if let folderFeed = sidebarItem as? Folder { @@ -638,7 +635,7 @@ private extension SidebarViewController { func rebuildTreeAndReloadDataIfNeeded() { if !animatingChanges && !BatchUpdate.shared.isPerforming { - updateReadFilterState() + treeControllerDelegate.isReadFiltered = isReadFiltered addAllSelectedToFilterExceptions() treeController.rebuild() treeControllerDelegate.resetFilterExceptions() @@ -647,14 +644,6 @@ private extension SidebarViewController { } } - var isReadFilterEnabled: Bool { - isReadFeedsFiltered || isReadArticlesFiltered - } - - func updateReadFilterState() { - treeControllerDelegate.isReadFiltered = isReadFilterEnabled - } - func expandNodes() { treeController.visitNodes(expandNodesVisitor(node:)) } diff --git a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift index e581ea199..c9586b5d5 100644 --- a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift @@ -130,6 +130,11 @@ final class TimelineContainerViewController: NSViewController { updateReadFilterButton() } + func setReadFilterEnabled(_ isEnabled: Bool) { + regularTimelineViewController.setReadFilterEnabled(isEnabled) + updateReadFilterButton() + } + // MARK: State Restoration func restoreState(from state: TimelineWindowState?) { diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 2c6832602..6dbccd37f 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -39,10 +39,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr guard timelineFeed.defaultReadFilterType != .alwaysRead else { return nil } - if let globalReadFilterEnabled { - return globalReadFilterEnabled - } - return timelineFeed.defaultReadFilterType == .read + return globalReadFilterEnabled } var isCleanUpAvailable: Bool { @@ -285,6 +282,17 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr fetchAndReplacePreservingSelection() } + func setReadFilterEnabled(_ isEnabled: Bool) { + guard globalReadFilterEnabled != isEnabled else { + return + } + globalReadFilterEnabled = isEnabled + AppDefaults.shared.timelineReadFilterEnabled = isEnabled + if representedObjects != nil { + fetchAndReplacePreservingSelection() + } + } + func toggleReadFilter() { guard let filter = isReadFiltered else { return @@ -324,8 +332,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr func restoreLegacyState(from state: [AnyHashable: Any]) { if let readArticlesFilterStateValues = state[UserInfoKey.readArticlesFilterStateValues] as? [Bool], let global = readArticlesFilterStateValues.first { - globalReadFilterEnabled = global - AppDefaults.shared.timelineReadFilterEnabled = global + setReadFilterEnabled(global) } if let articlePathUserInfo = state[UserInfoKey.articlePath] as? [AnyHashable: Any], @@ -1268,10 +1275,6 @@ private extension TimelineViewController { } func readFilterTableForCurrentFetchers(_ fetchers: [ArticleFetcher]) -> [SidebarItemIdentifier: Bool] { - guard let globalReadFilterEnabled else { - return [:] - } - var table = [SidebarItemIdentifier: Bool]() for fetcher in fetchers { guard let sidebarItem = fetcher as? SidebarItem, diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index 84286a5fa..63a1a28ab 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -67,7 +67,6 @@ final class AppDefaults: Sendable { static let currentThemeName = "currentThemeName" static let articleContentJavascriptEnabled = "articleContentJavascriptEnabled" static let hideReadFeeds = "hideReadFeeds" - static let hideReadArticles = "hideReadArticles" static let isShowingExtractedArticle = "isShowingExtractedArticle" static let articleWindowScrollY = "articleWindowScrollY" static let expandedContainers = "expandedContainers" @@ -277,15 +276,6 @@ final class AppDefaults: Sendable { } } - var hideReadArticles: Bool { - get { - UserDefaults.standard.bool(forKey: Key.hideReadArticles) - } - set { - UserDefaults.standard.set(newValue, forKey: Key.hideReadArticles) - } - } - var isShowingExtractedArticle: Bool { get { UserDefaults.standard.bool(forKey: Key.isShowingExtractedArticle) @@ -476,7 +466,6 @@ private extension AppDefaults { struct StateRestorationInfo { let hideReadFeeds: Bool - let hideReadArticles: Bool let expandedContainers: Set let selectedSidebarItem: SidebarItemIdentifier? let smartFeedsHidingReadArticles: Set @@ -487,7 +476,6 @@ struct StateRestorationInfo { let isShowingExtractedArticle: Bool init(hideReadFeeds: Bool, - hideReadArticles: Bool, expandedContainers: Set, selectedSidebarItem: SidebarItemIdentifier?, smartFeedsHidingReadArticles: Set, @@ -497,7 +485,6 @@ struct StateRestorationInfo { articleWindowScrollY: Int, isShowingExtractedArticle: Bool) { self.hideReadFeeds = hideReadFeeds - self.hideReadArticles = hideReadArticles self.expandedContainers = expandedContainers self.selectedSidebarItem = selectedSidebarItem self.smartFeedsHidingReadArticles = smartFeedsHidingReadArticles @@ -507,12 +494,11 @@ struct StateRestorationInfo { self.articleWindowScrollY = articleWindowScrollY self.isShowingExtractedArticle = isShowingExtractedArticle - AppDefaults.logger.debug("AppDefaults: StateRestorationInfo:\nhideReadArticles: \(hideReadArticles ? "true" : "false")\nexpandedContainers: \(expandedContainers)\nselectedSidebarItem: \(selectedSidebarItem?.userInfo ?? [String: String]())\nsmartFeedsHidingReadArticles: \(smartFeedsHidingReadArticles)\nfeedsHidingReadArticles: \(feedsHidingReadArticles)\nfoldersShowingReadArticles: \(foldersShowingReadArticles)\nselectedArticle: \(selectedArticle?.dictionary ?? [String: String]())\narticleWindowScrollY: \(articleWindowScrollY)\nisShowingExtractedArticle: \(isShowingExtractedArticle ? "true" : "false")") + AppDefaults.logger.debug("AppDefaults: StateRestorationInfo:\nexpandedContainers: \(expandedContainers)\nselectedSidebarItem: \(selectedSidebarItem?.userInfo ?? [String: String]())\nsmartFeedsHidingReadArticles: \(smartFeedsHidingReadArticles)\nfeedsHidingReadArticles: \(feedsHidingReadArticles)\nfoldersShowingReadArticles: \(foldersShowingReadArticles)\nselectedArticle: \(selectedArticle?.dictionary ?? [String: String]())\narticleWindowScrollY: \(articleWindowScrollY)\nisShowingExtractedArticle: \(isShowingExtractedArticle ? "true" : "false")") } init() { self.init(hideReadFeeds: AppDefaults.shared.hideReadFeeds, - hideReadArticles: AppDefaults.shared.hideReadArticles, expandedContainers: AppDefaults.shared.expandedContainers, selectedSidebarItem: AppDefaults.shared.selectedSidebarItem, smartFeedsHidingReadArticles: AppDefaults.shared.smartFeedsHidingReadArticles, @@ -582,17 +568,14 @@ struct StateRestorationInfo { var smartFeedsHidingReadArticles = Set() var feedsHidingReadArticles = [String: Set]() - var hideReadArticles = AppDefaults.shared.hideReadArticles for sidebarItem in sidebarItemsHidingReadArticles { switch sidebarItem { case .smartFeed(let id): smartFeedsHidingReadArticles.insert(id) - hideReadArticles = true case .feed(let accountID, let feedID): var feedIDs = feedsHidingReadArticles[accountID] ?? Set() feedIDs.insert(feedID) feedsHidingReadArticles[accountID] = feedIDs - hideReadArticles = true default: continue } @@ -607,7 +590,6 @@ struct StateRestorationInfo { } self.init(hideReadFeeds: hideReadFeeds, - hideReadArticles: hideReadArticles, expandedContainers: expandedContainers, selectedSidebarItem: selectedSidebarItem, smartFeedsHidingReadArticles: smartFeedsHidingReadArticles, diff --git a/iOS/HidingReadArticlesState.swift b/iOS/HidingReadArticlesState.swift index 324514c0e..842003c43 100644 --- a/iOS/HidingReadArticlesState.swift +++ b/iOS/HidingReadArticlesState.swift @@ -10,14 +10,11 @@ import Foundation import Account @MainActor final class HidingReadArticlesState { - private var hideReadArticles = AppDefaults.shared.hideReadArticles - - func copy(from stateRestorationInfo: StateRestorationInfo) { - hideReadArticles = stateRestorationInfo.hideReadArticles + func copy(from _: StateRestorationInfo) { + // Uses global read filter state from AppDefaults.hideReadFeeds. } func save() { - AppDefaults.shared.hideReadArticles = hideReadArticles // Clear per-sidebar-item legacy state now that read filtering is global. AppDefaults.shared.smartFeedsHidingReadArticles = [] AppDefaults.shared.feedsHidingReadArticles = [:] @@ -30,12 +27,12 @@ import Account return } - hideReadArticles.toggle() + AppDefaults.shared.hideReadFeeds.toggle() save() } func isHidingReadArticles(for sidebarItemID: SidebarItemIdentifier) -> Bool { - isUnreadSmartFeed(sidebarItemID) ? true : hideReadArticles + isUnreadSmartFeed(sidebarItemID) ? true : AppDefaults.shared.hideReadFeeds } func canToggleHidingReadArticles(for sidebarItemID: SidebarItemIdentifier) -> Bool { diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index feca7e731..25777c0a9 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -377,6 +377,7 @@ struct SidebarItemNode: Hashable, Sendable { // You can't assign the Feeds Read Filter until we've built the backing stores at least once or there is nothing // for state restoration to work with while we are waiting for the unread counts to initialize. + AppDefaults.shared.hideReadFeeds = stateInfo.hideReadFeeds treeControllerDelegate.isReadFiltered = stateInfo.hideReadFeeds restoreSelectedSidebarItemAndArticle(stateInfo) @@ -757,6 +758,7 @@ struct SidebarItemNode: Hashable, Sendable { treeControllerDelegate.isReadFiltered = newValue AppDefaults.shared.hideReadFeeds = newValue rebuildBackingStores() + refreshTimeline(resetScroll: false) mainFeedCollectionViewController?.updateUI() } @@ -772,6 +774,9 @@ struct SidebarItemNode: Hashable, Sendable { return } hidingReadArticlesState.toggleHidingReadArticles(for: sidebarItemID) + treeControllerDelegate.isReadFiltered = AppDefaults.shared.hideReadFeeds + rebuildBackingStores() + mainFeedCollectionViewController?.updateUI() refreshTimeline(resetScroll: false) }