forked from shadowfacts/Tusker
Reapply filters on when they change
This commit is contained in:
parent
fabe339215
commit
6501343f24
|
@ -10,14 +10,15 @@ import Foundation
|
|||
import Pachyderm
|
||||
import Combine
|
||||
|
||||
/// An opaque object that serves as the cache for the filtered-ness of a particular status.
|
||||
class FilterState {
|
||||
static var unknown: FilterState { FilterState(state: .unknown) }
|
||||
|
||||
private var state: State
|
||||
fileprivate var state: State
|
||||
|
||||
var isWarning: Bool {
|
||||
switch state {
|
||||
case .known(.warn(_)):
|
||||
case .known(.warn(_), _):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
@ -28,26 +29,9 @@ class FilterState {
|
|||
self.state = state
|
||||
}
|
||||
|
||||
// Use a closure for the status in case the result is cached and we don't need to look it up
|
||||
@MainActor
|
||||
func resolveFor(status: () -> StatusMO, resolver: Filterer) -> Filterer.Result {
|
||||
switch state {
|
||||
case .unknown:
|
||||
let result = resolver.resolve(status: status())
|
||||
state = .known(result)
|
||||
return result
|
||||
case .known(let result):
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func setResult(_ result: Filterer.Result) {
|
||||
self.state = .known(result)
|
||||
}
|
||||
|
||||
private enum State {
|
||||
fileprivate enum State {
|
||||
case unknown
|
||||
case known(Filterer.Result)
|
||||
case known(Filterer.Result, generation: Int)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,24 +40,33 @@ class Filterer {
|
|||
let mastodonController: MastodonController
|
||||
let context: FilterV1.Context
|
||||
|
||||
var filtersChanged: ((Bool) -> Void)?
|
||||
|
||||
private let htmlConverter = HTMLConverter()
|
||||
private var needsSetupFilters = true
|
||||
private var hasSetup = false
|
||||
private var matchers = [(NSRegularExpression, Result)]()
|
||||
private var filtersChanged: AnyCancellable?
|
||||
private var allFiltersObserver: AnyCancellable?
|
||||
private var filterObservers = Set<AnyCancellable>()
|
||||
|
||||
// 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
|
||||
|
||||
init(mastodonController: MastodonController, context: FilterV1.Context) {
|
||||
self.mastodonController = mastodonController
|
||||
self.context = context
|
||||
|
||||
filtersChanged = mastodonController.$filters
|
||||
.sink { [unowned self] _ in
|
||||
self.needsSetupFilters = true
|
||||
allFiltersObserver = mastodonController.$filters
|
||||
.sink { [unowned self] in
|
||||
if self.hasSetup {
|
||||
self.setupFilters(filters: $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupFilters(filters: [FilterMO]) {
|
||||
print("setting up filters")
|
||||
let oldMatchers = matchers
|
||||
|
||||
matchers = []
|
||||
filterObservers = []
|
||||
for filter in filters where filter.contexts.contains(context) {
|
||||
|
@ -84,15 +77,62 @@ class Filterer {
|
|||
|
||||
filter.objectWillChange
|
||||
.sink { [unowned self] _ in
|
||||
self.needsSetupFilters = true
|
||||
// wait until after the change happens
|
||||
DispatchQueue.main.async {
|
||||
self.setupFilters(filters: self.mastodonController.filters)
|
||||
}
|
||||
}
|
||||
.store(in: &filterObservers)
|
||||
}
|
||||
needsSetupFilters = false
|
||||
|
||||
if hasSetup {
|
||||
var allMatch: Bool = false
|
||||
var actionsChanged: Bool = false
|
||||
if matchers.count != oldMatchers.count {
|
||||
allMatch = false
|
||||
actionsChanged = true
|
||||
} else {
|
||||
for (old, new) in zip(oldMatchers, matchers) {
|
||||
if old.1 != new.1 {
|
||||
allMatch = false
|
||||
actionsChanged = true
|
||||
break
|
||||
} else if old.0.pattern != new.0.pattern {
|
||||
allMatch = false
|
||||
// continue because we want to know if any actions changed
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if !allMatch {
|
||||
generation += 1
|
||||
filtersChanged?(actionsChanged)
|
||||
}
|
||||
} else {
|
||||
hasSetup = true
|
||||
}
|
||||
}
|
||||
|
||||
func resolve(status: StatusMO) -> Result {
|
||||
if needsSetupFilters {
|
||||
// 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 {
|
||||
switch state.state {
|
||||
case .known(_, generation: let knownGen) where knownGen < generation:
|
||||
fallthrough
|
||||
case .unknown:
|
||||
let result = doResolve(status: status())
|
||||
state.state = .known(result, generation: generation)
|
||||
return result
|
||||
case .known(let result, _):
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func setResult(_ result: Result, for state: FilterState) {
|
||||
state.state = .known(result, generation: generation)
|
||||
}
|
||||
|
||||
private func doResolve(status: StatusMO) -> Result {
|
||||
if !hasSetup {
|
||||
setupFilters(filters: mastodonController.filters)
|
||||
}
|
||||
if matchers.isEmpty {
|
||||
|
@ -108,7 +148,7 @@ class Filterer {
|
|||
return .allow
|
||||
}
|
||||
|
||||
enum Result {
|
||||
enum Result: Equatable {
|
||||
case allow
|
||||
case hide
|
||||
case warn(String)
|
||||
|
|
|
@ -108,6 +108,10 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
|||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
filterer.filtersChanged = { [unowned self] actionsChanged in
|
||||
self.reapplyFilters(actionsChanged: actionsChanged)
|
||||
}
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
|
@ -142,12 +146,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
|||
return cell
|
||||
}
|
||||
case .status(id: let id, collapseState: let collapseState, filterState: let filterState, pinned: let pinned):
|
||||
let status = {
|
||||
let status = self.mastodonController.persistentContainer.status(for: id)!
|
||||
// if the status is a reblog of another one, filter based on that one
|
||||
return status.reblog ?? status
|
||||
}
|
||||
let result = filterState.resolveFor(status: status, resolver: filterer)
|
||||
let result = filterResult(state: filterState, statusID: id)
|
||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, collapseState, result, pinned))
|
||||
case .loadingIndicator:
|
||||
return loadingIndicatorCell(for: indexPath)
|
||||
|
@ -246,6 +245,38 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
|||
await apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
|
||||
private func filterResult(state: FilterState, statusID: String) -> Filterer.Result {
|
||||
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
|
||||
}
|
||||
return filterer.resolve(state: state, status: status)
|
||||
}
|
||||
|
||||
private func reapplyFilters(actionsChanged: Bool) {
|
||||
let visible = collectionView.indexPathsForVisibleItems
|
||||
let items = visible
|
||||
.compactMap { dataSource.itemIdentifier(for: $0) }
|
||||
.filter {
|
||||
if case .status(_, _, _, _) = $0 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
guard !items.isEmpty else {
|
||||
return
|
||||
}
|
||||
var snapshot = dataSource.snapshot()
|
||||
if actionsChanged {
|
||||
snapshot.reloadItems(items)
|
||||
} else {
|
||||
snapshot.reconfigureItems(items)
|
||||
}
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
@objc func refresh() {
|
||||
guard case .loaded = state else {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
|
@ -458,7 +489,7 @@ extension ProfileStatusesViewController: UICollectionViewDelegate {
|
|||
return
|
||||
}
|
||||
if filterState.isWarning {
|
||||
filterState.setResult(.allow)
|
||||
filterer.setResult(.allow, for: filterState)
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reconfigureItems([item])
|
||||
|
@ -504,7 +535,7 @@ extension ProfileStatusesViewController: StatusCollectionViewCellDelegate {
|
|||
if let indexPath = collectionView.indexPath(for: cell),
|
||||
let item = dataSource.itemIdentifier(for: indexPath),
|
||||
case .status(id: _, collapseState: _, filterState: let filterState, pinned: _) = item {
|
||||
filterState.setResult(.allow)
|
||||
filterer.setResult(.allow, for: filterState)
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reconfigureItems([item])
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
|
|
|
@ -69,7 +69,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
config.topSeparatorVisibility = .hidden
|
||||
config.bottomSeparatorVisibility = .hidden
|
||||
} else if case .status(id: let id, collapseState: _, filterState: let filterState) = item,
|
||||
case .hide = filterState.resolveFor(status: { mastodonController.persistentContainer.status(for: id)! }, resolver: filterer) {
|
||||
case .hide = filterResult(state: filterState, statusID: id) {
|
||||
config.topSeparatorVisibility = .hidden
|
||||
config.bottomSeparatorVisibility = .hidden
|
||||
} else {
|
||||
|
@ -108,6 +108,10 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
}
|
||||
}
|
||||
|
||||
filterer.filtersChanged = { [unowned self] actionsChanged in
|
||||
self.reapplyFilters(actionsChanged: actionsChanged)
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(sceneWillEnterForeground), name: UIScene.willEnterForegroundNotification, object: nil)
|
||||
}
|
||||
|
||||
|
@ -144,12 +148,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
||||
switch itemIdentifier {
|
||||
case .status(id: let id, collapseState: let state, filterState: let filterState):
|
||||
let status = {
|
||||
let status = self.mastodonController.persistentContainer.status(for: id)!
|
||||
// if the status is a reblog of another one, filter based on that one
|
||||
return status.reblog ?? status
|
||||
}
|
||||
let result = filterState.resolveFor(status: status, resolver: filterer)
|
||||
let result = filterResult(state: filterState, statusID: id)
|
||||
switch result {
|
||||
case .allow, .warn(_):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state, result))
|
||||
|
@ -330,6 +329,40 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
isShowingTimelineDescription = false
|
||||
}
|
||||
|
||||
private func filterResult(state: FilterState, statusID: String) -> Filterer.Result {
|
||||
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
|
||||
}
|
||||
return filterer.resolve(state: state, status: status)
|
||||
}
|
||||
|
||||
private func reapplyFilters(actionsChanged: Bool) {
|
||||
let visible = collectionView.indexPathsForVisibleItems
|
||||
let items = visible
|
||||
.compactMap { dataSource.itemIdentifier(for: $0) }
|
||||
.filter {
|
||||
if case .status(_, _, _) = $0 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
guard !items.isEmpty else {
|
||||
return
|
||||
}
|
||||
var snapshot = dataSource.snapshot()
|
||||
if actionsChanged {
|
||||
// need to reload not just reconfigure because hidden posts use a separate cell type
|
||||
snapshot.reloadItems(items)
|
||||
} else {
|
||||
// reconfigure when possible to avoid the content offset jumping around
|
||||
snapshot.reconfigureItems(items)
|
||||
}
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
@objc private func sceneWillEnterForeground(_ notification: Foundation.Notification) {
|
||||
guard let scene = notification.object as? UIScene,
|
||||
// view.window is nil when the VC is not on screen
|
||||
|
@ -790,7 +823,7 @@ extension TimelineViewController: UICollectionViewDelegate {
|
|||
removeTimelineDescriptionCell()
|
||||
case .status(id: let id, collapseState: let collapseState, filterState: let filterState):
|
||||
if filterState.isWarning {
|
||||
filterState.setResult(.allow)
|
||||
filterer.setResult(.allow, for: filterState)
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reconfigureItems([item])
|
||||
|
@ -853,7 +886,7 @@ extension TimelineViewController: StatusCollectionViewCellDelegate {
|
|||
if let indexPath = collectionView.indexPath(for: cell),
|
||||
let item = dataSource.itemIdentifier(for: indexPath),
|
||||
case .status(id: _, collapseState: _, filterState: let filterState) = item {
|
||||
filterState.setResult(.allow)
|
||||
filterer.setResult(.allow, for: filterState)
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reconfigureItems([item])
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
|
|
Loading…
Reference in New Issue