From ce708e2d16f44bde2ea0a887d3de54f5bd0bc5cc Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 17 Dec 2022 13:40:15 -0500 Subject: [PATCH] Hide reblogs and hide replies filters Closes #202 --- Tusker/Filterer.swift | 49 ++++++++++++++++--- Tusker/Preferences/Preferences.swift | 8 +++ .../TrendingStatusesViewController.swift | 2 +- Tusker/Screens/Filters/FiltersView.swift | 12 +++++ .../Preferences/BehaviorPrefsView.swift | 6 +++ .../ProfileStatusesViewController.swift | 6 ++- .../Timeline/TimelineViewController.swift | 6 ++- 7 files changed, 80 insertions(+), 9 deletions(-) diff --git a/Tusker/Filterer.swift b/Tusker/Filterer.swift index 55bcee00..31663d70 100644 --- a/Tusker/Filterer.swift +++ b/Tusker/Filterer.swift @@ -45,9 +45,12 @@ class Filterer { var htmlConverter = HTMLConverter() private var hasSetup = false private var matchers = [(NSRegularExpression, Result)]() - private var allFiltersObserver: AnyCancellable? + private var cancellables = Set() private var filterObservers = Set() + private var hideReblogsInTimelines: Bool + private var hideRepliesInTimelines: Bool + // the generation is incremented when the matchers change, to indicate that older cached FilterStates // are no longer valid, without needing to go through and update each of them private var generation = 0 @@ -55,13 +58,37 @@ class Filterer { init(mastodonController: MastodonController, context: FilterV1.Context) { self.mastodonController = mastodonController self.context = context + self.hideReblogsInTimelines = Preferences.shared.hideReblogsInTimelines + self.hideRepliesInTimelines = Preferences.shared.hideRepliesInTimelines - allFiltersObserver = mastodonController.$filters + mastodonController.$filters .sink { [unowned self] in if self.hasSetup { self.setupFilters(filters: $0) } } + .store(in: &cancellables) + + if context == .home { + Preferences.shared.$hideReblogsInTimelines + .sink { [unowned self] newValue in + if newValue != hideReblogsInTimelines { + self.hideReblogsInTimelines = newValue + self.generation += 1 + self.filtersChanged?(true) + } + } + .store(in: &cancellables) + Preferences.shared.$hideRepliesInTimelines + .sink { [unowned self] newValue in + if newValue != hideRepliesInTimelines { + self.hideRepliesInTimelines = newValue + self.generation += 1 + self.filtersChanged?(true) + } + } + .store(in: &cancellables) + } } private func setupFilters(filters: [FilterMO]) { @@ -86,7 +113,7 @@ class Filterer { } if hasSetup { - var allMatch: Bool = false + var allMatch: Bool = true var actionsChanged: Bool = false if matchers.count != oldMatchers.count { allMatch = false @@ -114,12 +141,13 @@ class Filterer { } // Use a closure for the status in case the result is cached and we don't need to look it up - func resolve(state: FilterState, status: () -> StatusMO) -> (Filterer.Result, NSAttributedString?) { + func resolve(state: FilterState, status: () -> (StatusMO, Bool)) -> (Filterer.Result, NSAttributedString?) { switch state.state { case .known(_, generation: let knownGen) where knownGen < generation: fallthrough case .unknown: - let (result, attributedString) = doResolve(status: status()) + let (status, isReblog) = status() + let (result, attributedString) = doResolve(status: status, isReblog: isReblog) state.state = .known(result, generation: generation) return (result, attributedString) case .known(let result, _): @@ -140,10 +168,19 @@ class Filterer { } } - private func doResolve(status: StatusMO) -> (Result, NSAttributedString?) { + private func doResolve(status: StatusMO, isReblog: Bool) -> (Result, NSAttributedString?) { if !hasSetup { setupFilters(filters: mastodonController.filters) } + if context == .home { + if hideReblogsInTimelines, + isReblog { + return (.hide, nil) + } else if hideRepliesInTimelines, + status.inReplyToID != nil { + return (.hide, nil) + } + } if matchers.isEmpty { return (.allow, nil) } diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index 73f390b0..65d8dbf9 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -70,6 +70,8 @@ class Preferences: Codable, ObservableObject { self.oppositeCollapseKeywords = try container.decodeIfPresent([String].self, forKey: .oppositeCollapseKeywords) ?? [] self.confirmBeforeReblog = try container.decodeIfPresent(Bool.self, forKey: .confirmBeforeReblog) ?? false self.timelineStateRestoration = try container.decodeIfPresent(Bool.self, forKey: .timelineStateRestoration) ?? true + self.hideReblogsInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideReblogsInTimelines) ?? false + self.hideRepliesInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideRepliesInTimelines) ?? false self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts) self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType) @@ -115,6 +117,8 @@ class Preferences: Codable, ObservableObject { try container.encode(oppositeCollapseKeywords, forKey: .oppositeCollapseKeywords) try container.encode(confirmBeforeReblog, forKey: .confirmBeforeReblog) try container.encode(timelineStateRestoration, forKey: .timelineStateRestoration) + try container.encode(hideReblogsInTimelines, forKey: .hideReblogsInTimelines) + try container.encode(hideRepliesInTimelines, forKey: .hideRepliesInTimelines) try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts) try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType) @@ -169,6 +173,8 @@ class Preferences: Codable, ObservableObject { @Published var oppositeCollapseKeywords: [String] = [] @Published var confirmBeforeReblog = false @Published var timelineStateRestoration = true + @Published var hideReblogsInTimelines = false + @Published var hideRepliesInTimelines = false // MARK: Digital Wellness @Published var showFavoriteAndReblogCounts = true @@ -216,6 +222,8 @@ class Preferences: Codable, ObservableObject { case oppositeCollapseKeywords case confirmBeforeReblog case timelineStateRestoration + case hideReblogsInTimelines + case hideRepliesInTimelines case showFavoriteAndReblogCounts case defaultNotificationsType diff --git a/Tusker/Screens/Explore/TrendingStatusesViewController.swift b/Tusker/Screens/Explore/TrendingStatusesViewController.swift index 6a8bd5fe..8b98ed4e 100644 --- a/Tusker/Screens/Explore/TrendingStatusesViewController.swift +++ b/Tusker/Screens/Explore/TrendingStatusesViewController.swift @@ -81,7 +81,7 @@ class TrendingStatusesViewController: UIViewController { return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in switch itemIdentifier { case .status(id: let id, let collapseState, let filterState): - let (result, attributedString) = self.filterer.resolve(state: filterState, status: { mastodonController.persistentContainer.status(for: id)! }) + let (result, attributedString) = self.filterer.resolve(state: filterState, status: { (mastodonController.persistentContainer.status(for: id)!, false) }) switch result { case .allow, .warn(_): return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, collapseState, result, attributedString)) diff --git a/Tusker/Screens/Filters/FiltersView.swift b/Tusker/Screens/Filters/FiltersView.swift index 1552107b..cbdc0bbf 100644 --- a/Tusker/Screens/Filters/FiltersView.swift +++ b/Tusker/Screens/Filters/FiltersView.swift @@ -22,6 +22,7 @@ struct FiltersView: View { struct FiltersList: View { @EnvironmentObject private var mastodonController: MastodonController + @ObservedObject private var preferences = Preferences.shared @FetchRequest(sortDescriptors: []) private var filters: FetchedResults @Environment(\.dismiss) private var dismiss @State private var deletionError: (any Error)? @@ -49,6 +50,17 @@ struct FiltersList: View { private var navigationBody: some View { List { + Section { + Toggle(isOn: $preferences.hideReblogsInTimelines) { + Text("Hide Reblogs") + } + Toggle(isOn: $preferences.hideRepliesInTimelines) { + Text("Hide Replies") + } + } header: { + Text("Home Timeline") + } + Section { NavigationLink { EditFilterView(filter: EditedFilter(), create: true, originallyExpired: false) diff --git a/Tusker/Screens/Preferences/BehaviorPrefsView.swift b/Tusker/Screens/Preferences/BehaviorPrefsView.swift index 96251c63..09dcdadc 100644 --- a/Tusker/Screens/Preferences/BehaviorPrefsView.swift +++ b/Tusker/Screens/Preferences/BehaviorPrefsView.swift @@ -35,6 +35,12 @@ struct BehaviorPrefsView: View { Toggle(isOn: $preferences.timelineStateRestoration) { Text("Maintain Position Across App Launches") } + Toggle(isOn: $preferences.hideReblogsInTimelines) { + Text("Hide Reblogs") + } + Toggle(isOn: $preferences.hideRepliesInTimelines) { + Text("Hide Replies") + } } header: { Text("Timeline") } diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift index 1ea9b472..66658da3 100644 --- a/Tusker/Screens/Profile/ProfileStatusesViewController.swift +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -261,7 +261,11 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie let status = { let status = self.mastodonController.persistentContainer.status(for: statusID)! // if the status is a reblog of another one, filter based on that one - return status.reblog ?? status + if let reblogged = status.reblog { + return (reblogged, true) + } else { + return (status, false) + } } return filterer.resolve(state: state, status: status) } diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index 6b4883ea..9a081ca4 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -354,7 +354,11 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro let status = { let status = self.mastodonController.persistentContainer.status(for: statusID)! // if the status is a reblog of another one, filter based on that one - return status.reblog ?? status + if let reblogged = status.reblog { + return (reblogged, true) + } else { + return (status, false) + } } return filterer.resolve(state: state, status: status) }