diff --git a/Pachyderm/Sources/Pachyderm/Model/Filter.swift b/Pachyderm/Sources/Pachyderm/Model/Filter.swift index 92e05fad..41b609ea 100644 --- a/Pachyderm/Sources/Pachyderm/Model/Filter.swift +++ b/Pachyderm/Sources/Pachyderm/Model/Filter.swift @@ -8,7 +8,7 @@ import Foundation -public struct Filter: Decodable { +public struct Filter: FilterProtocol, Decodable { public let id: String public let phrase: String private let context: [String] @@ -22,17 +22,17 @@ public struct Filter: Decodable { } } - public static func update(_ filter: Filter, phrase: String? = nil, context: [Context]? = nil, irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request { + public static func update(_ filter: some FilterProtocol, phrase: String? = nil, context: [Context]? = nil, irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresIn: TimeInterval? = nil) -> Request { return Request(method: .put, path: "/api/v1/filters/\(filter.id)", body: ParametersBody([ "phrase" => (phrase ?? filter.phrase), "irreversible" => (irreversible ?? filter.irreversible), "whole_word" => (wholeWord ?? filter.wholeWord), - "expires_at" => (expiresAt ?? filter.expiresAt) - ] + "context" => (context?.contextStrings ?? filter.context))) + "expires_in" => (expiresIn ?? filter.expiresAt?.timeIntervalSinceNow), + ] + "context" => (context ?? filter.contexts).contextStrings)) } - public static func delete(_ filter: Filter) -> Request { - return Request(method: .delete, path: "/api/v1/filters/\(filter.id)") + public static func delete(_ filterID: String) -> Request { + return Request(method: .delete, path: "/api/v1/filters/\(filterID)") } private enum CodingKeys: String, CodingKey { @@ -46,7 +46,7 @@ public struct Filter: Decodable { } extension Filter { - public enum Context: String, Decodable { + public enum Context: String, Decodable, CaseIterable { case home case notifications case `public` diff --git a/Pachyderm/Sources/Pachyderm/Model/Protocols/FilterProtocol.swift b/Pachyderm/Sources/Pachyderm/Model/Protocols/FilterProtocol.swift new file mode 100644 index 00000000..5bf091b5 --- /dev/null +++ b/Pachyderm/Sources/Pachyderm/Model/Protocols/FilterProtocol.swift @@ -0,0 +1,17 @@ +// +// FilterProtocol.swift +// Pachyderm +// +// Created by Shadowfacts on 12/2/22. +// + +import Foundation + +public protocol FilterProtocol { + var id: String { get } + var phrase: String { get } + var contexts: [Filter.Context] { get } + var expiresAt: Date? { get } + var irreversible: Bool { get } + var wholeWord: Bool { get } +} diff --git a/Pachyderm/Sources/Pachyderm/Request/Parameter.swift b/Pachyderm/Sources/Pachyderm/Request/Parameter.swift index b5371b17..82b65aba 100644 --- a/Pachyderm/Sources/Pachyderm/Request/Parameter.swift +++ b/Pachyderm/Sources/Pachyderm/Request/Parameter.swift @@ -42,6 +42,10 @@ extension String { } } + static func =>(name: String, value: TimeInterval?) -> Parameter { + return name => (value == nil ? nil : Int(value!)) + } + static func =>(name: String, focus: (Float, Float)?) -> Parameter { guard let focus = focus else { return Parameter(name: name, value: nil) } return Parameter(name: name, value: "\(focus.0),\(focus.1)") diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 665d8fda..27d85b92 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -52,9 +52,13 @@ D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; }; D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; }; D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */; }; + D61F759929384D4D00C0B37F /* FiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759829384D4D00C0B37F /* FiltersView.swift */; }; D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759A29384F9C00C0B37F /* FilterMO.swift */; }; + D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759C2938574B00C0B37F /* FilterRow.swift */; }; D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */; }; D61F75A129396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift */; }; + D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75A4293ABD6F00C0B37F /* EditFilterView.swift */; }; + D61F75AD293AF39000C0B37F /* FilterContext+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75AC293AF39000C0B37F /* FilterContext+Helpers.swift */; }; D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; }; D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; }; D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; }; @@ -423,9 +427,13 @@ D61F759129365C6C00C0B37F /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = ""; }; D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedHashtag.swift; sourceTree = ""; }; D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleFollowHashtagService.swift; sourceTree = ""; }; + D61F759829384D4D00C0B37F /* FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersView.swift; sourceTree = ""; }; D61F759A29384F9C00C0B37F /* FilterMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterMO.swift; sourceTree = ""; }; + D61F759C2938574B00C0B37F /* FilterRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterRow.swift; sourceTree = ""; }; D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiCaseSensitiveComparator.swift; sourceTree = ""; }; D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiCaseSensitiveComparatorTests.swift; sourceTree = ""; }; + D61F75A4293ABD6F00C0B37F /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = ""; }; + D61F75AC293AF39000C0B37F /* FilterContext+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilterContext+Helpers.swift"; sourceTree = ""; }; D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = ""; }; D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = ""; }; D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = ""; }; @@ -794,6 +802,16 @@ path = "Instance Cell"; sourceTree = ""; }; + D61F759729384D4200C0B37F /* Filters */ = { + isa = PBXGroup; + children = ( + D61F759829384D4D00C0B37F /* FiltersView.swift */, + D61F759C2938574B00C0B37F /* FilterRow.swift */, + D61F75A4293ABD6F00C0B37F /* EditFilterView.swift */, + ); + path = Filters; + sourceTree = ""; + }; D623A53B2635F4E20095BD04 /* Poll */ = { isa = PBXGroup; children = ( @@ -922,6 +940,7 @@ D6F2E960249E772F005846BB /* Crash Reporter */, D627943C23A5635D00D38C68 /* Explore */, D6A4DCC92553666600D9DE31 /* Fast Account Switcher */, + D61F759729384D4200C0B37F /* Filters */, D641C788213DD86D004B4513 /* Large Image */, D627944B23A9A02400D38C68 /* Lists */, D641C782213DD7F0004B4513 /* Main */, @@ -1183,6 +1202,7 @@ D6ADB6ED28EA74E8009924AB /* UIView+Configure.swift */, D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */, D61F758F29353B4300C0B37F /* FileManager+Size.swift */, + D61F75AC293AF39000C0B37F /* FilterContext+Helpers.swift */, ); path = Extensions; sourceTree = ""; @@ -1821,6 +1841,7 @@ D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */, 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */, + D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */, D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */, D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */, D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */, @@ -1876,6 +1897,7 @@ D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */, D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */, D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */, + D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */, D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */, D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */, D60E2F272442372B005F8713 /* StatusMO.swift in Sources */, @@ -1903,6 +1925,7 @@ D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, D6BEA249291C6118002F4D01 /* DraftsView.swift in Sources */, + D61F75AD293AF39000C0B37F /* FilterContext+Helpers.swift in Sources */, D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */, D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */, D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */, @@ -2012,6 +2035,7 @@ D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */, D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */, + D61F759929384D4D00C0B37F /* FiltersView.swift in Sources */, D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */, D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */, 04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */, diff --git a/Tusker/CoreData/FilterMO.swift b/Tusker/CoreData/FilterMO.swift index af40620b..ac4605d1 100644 --- a/Tusker/CoreData/FilterMO.swift +++ b/Tusker/CoreData/FilterMO.swift @@ -11,7 +11,7 @@ import CoreData import Pachyderm @objc(FilterMO) -public final class FilterMO: NSManagedObject { +public final class FilterMO: NSManagedObject, FilterProtocol { @nonobjc public class func fetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "Filter") @@ -25,7 +25,7 @@ public final class FilterMO: NSManagedObject { @NSManaged public var wholeWord: Bool private var _contexts: [Filter.Context]? - var contexts: [Filter.Context] { + public var contexts: [Filter.Context] { get { if let _contexts { return _contexts diff --git a/Tusker/Extensions/FilterContext+Helpers.swift b/Tusker/Extensions/FilterContext+Helpers.swift new file mode 100644 index 00000000..c8ead619 --- /dev/null +++ b/Tusker/Extensions/FilterContext+Helpers.swift @@ -0,0 +1,26 @@ +// +// FilterContext+Helpers.swift +// Tusker +// +// Created by Shadowfacts on 12/2/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import Pachyderm + +extension Filter.Context { + var displayName: String { + switch self { + case .home: + return "Home and lists" + case .notifications: + return "Notifications" + case .public: + return "Public timelines" + case .thread: + return "Conversations" + case .account: + return "Profiles" + } + } +} diff --git a/Tusker/Screens/Filters/EditFilterView.swift b/Tusker/Screens/Filters/EditFilterView.swift new file mode 100644 index 00000000..49c5fb8e --- /dev/null +++ b/Tusker/Screens/Filters/EditFilterView.swift @@ -0,0 +1,144 @@ +// +// EditFilterView.swift +// Tusker +// +// Created by Shadowfacts on 12/2/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import SwiftUI +import Pachyderm + +struct EditFilterView: View { + private static let expiresInOptions: [MenuPicker.Option] = { + let f = DateComponentsFormatter() + f.maximumUnitCount = 1 + f.unitsStyle = .full + f.allowedUnits = [.weekOfMonth, .day, .hour, .minute] + + let durations: [TimeInterval] = [ + 30 * 60, + 60 * 60, + 6 * 60 * 60, + 24 * 60 * 60, + 3 * 24 * 60 * 60, + 7 * 24 * 60 * 60, + ] + return durations.map { .init(value: $0, title: f.string(from: $0)!) } + }() + + @ObservedObject var filter: FilterMO + let updateFilter: () -> Void + @EnvironmentObject private var mastodonController: MastodonController + @State private var edited = false + + init(filter: FilterMO, updateFilter: @escaping () -> Void) { + self.filter = filter + self.updateFilter = updateFilter + if let expiresAt = filter.expiresAt { + let dist = expiresAt.timeIntervalSinceNow + self.expiresIn = Self.expiresInOptions.min(by: { a, b in + let aDist = abs(a.value - dist) + let bDist = abs(b.value - dist) + return aDist < bDist + })!.value + } else { + self.expiresIn = 24 * 60 * 60 + } + } + + @State private var expiresIn: TimeInterval { + didSet { + if filter.expiresAt != nil { + filter.expiresAt = Date(timeIntervalSinceNow: expiresIn) + } + } + } + private var expires: Binding { + Binding { + filter.expiresAt != nil + } set: { newValue in + if newValue { + filter.expiresAt = Date(timeIntervalSinceNow: expiresIn) + } else { + filter.expiresAt = nil + } + } + } + + var body: some View { + Form { + Section { + TextField("Phrase", text: $filter.phrase) + Toggle("Whole Word", isOn: $filter.wholeWord) + } + + Section { + Toggle("Expires", isOn: expires) + if expires.wrappedValue { + Picker(selection: $expiresIn) { + ForEach(Self.expiresInOptions, id: \.value) { option in + Text(option.title).tag(option.value) + } + } label: { + Text("Duration") + } + } + } + + Section { + ForEach(Filter.Context.allCases, id: \.rawValue) { context in + Toggle(isOn: Binding(get: { + filter.contexts.contains(context) + }, set: { newValue in + if newValue { + if !filter.contexts.contains(context) { + filter.contexts.append(context) + } + } else if filter.contexts.count > 1 { + filter.contexts.removeAll(where: { $0 == context }) + } + })) { + Text(context.displayName) + } + .toggleStyle(FilterContextToggleStyle()) + } + } header: { + Text("Contexts") + } + } + .navigationTitle("Edit Filter") + .navigationBarTitleDisplayMode(.inline) + .onReceive(filter.objectWillChange, perform: { _ in + edited = true + }) + .onDisappear { + if edited { + updateFilter() + } + } + } +} + +private struct FilterContextToggleStyle: ToggleStyle { + func makeBody(configuration: Configuration) -> some View { + Button { + configuration.isOn.toggle() + } label: { + HStack { + configuration.label + .foregroundColor(.primary) + Spacer() + if configuration.isOn { + Image(systemName: "checkmark") + } + } + } + } +} + +//struct EditFilterView_Previews: PreviewProvider { +// static var previews: some View { +// EditFilterView() +// } +//} diff --git a/Tusker/Screens/Filters/FilterRow.swift b/Tusker/Screens/Filters/FilterRow.swift new file mode 100644 index 00000000..cb16f5f8 --- /dev/null +++ b/Tusker/Screens/Filters/FilterRow.swift @@ -0,0 +1,59 @@ +// +// FilterRow.swift +// Tusker +// +// Created by Shadowfacts on 11/30/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import SwiftUI +import Pachyderm + +struct FilterRow: View { + @ObservedObject var filter: FilterMO + + var body: some View { + VStack(alignment: .leading) { + HStack(alignment: .top) { + Text(filter.phrase) + .font(.headline) + + Spacer() + + if let expiresAt = filter.expiresAt { + if expiresAt <= Date() { + Text("Expired") + .font(.body.lowercaseSmallCaps()) + .foregroundColor(.red) + } else { + Text(expiresAt.formatted(.relative(presentation: .numeric, unitsStyle: .narrow))) + .font(.body.lowercaseSmallCaps()) + } + } + } + + // rather than mapping over filter.contexts, because we want a consistent order + Text(Filter.Context.allCases.filter { filter.contexts.contains($0) }.map(\.displayName).formatted()) + .font(.subheadline) + + if filter.wholeWord { + Text("Whole word") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + } +} + +struct FilterRow_Previews: PreviewProvider { + static var previews: some View { + let filter = FilterMO() + filter.id = "1" + filter.phrase = "test" + filter.expiresAt = Date().addingTimeInterval(60 * 60) + filter.wholeWord = true + filter.irreversible = false + filter.contexts = [.home] + return FilterRow(filter: filter) + } +} diff --git a/Tusker/Screens/Filters/FiltersView.swift b/Tusker/Screens/Filters/FiltersView.swift new file mode 100644 index 00000000..648e8b50 --- /dev/null +++ b/Tusker/Screens/Filters/FiltersView.swift @@ -0,0 +1,142 @@ +// +// FiltersView.swift +// Tusker +// +// Created by Shadowfacts on 11/30/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import SwiftUI +import Pachyderm + +struct FiltersView: View { + let mastodonController: MastodonController + + var body: some View { + FiltersList(mastodonController: mastodonController) + .environment(\.managedObjectContext, mastodonController.persistentContainer.viewContext) + } + +} + +struct FiltersList: View { + let mastodonController: MastodonController + @FetchRequest(sortDescriptors: []) private var filters: FetchedResults + @Environment(\.dismiss) private var dismiss + @State private var deletionError: (any Error)? + @State private var updatingError: (any Error)? + + var body: some View { + if #available(iOS 16.0, *) { + NavigationStack { + navigationBody + } + } else { + NavigationView { + navigationBody + } + .navigationViewStyle(.stack) + } + } + + private var unexpiredFilters: [FilterMO] { + filters.filter { $0.expiresAt == nil || $0.expiresAt! > Date() }.sorted(using: SemiCaseSensitiveComparator.keyPath(\.phrase)) + } + + private var expiredFilters: [FilterMO] { + filters.filter { $0.expiresAt != nil && $0.expiresAt! <= Date() }.sorted(using: SemiCaseSensitiveComparator.keyPath(\.phrase)) + } + + private var navigationBody: some View { + List { + filtersSection(unexpiredFilters) + filtersSection(expiredFilters) + } + .navigationTitle(Text("Filters")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + dismiss() + } + } + } + .alertWithData("Error Deleting Filter", data: $deletionError, actions: { _ in + Button("OK") { + self.deletionError = nil + } + }, message: { error in + Text(error.localizedDescription) + }) + .alertWithData("Error Update Filter", data: $updatingError, actions: { _ in + Button("OK") { + self.updatingError = nil + } + }, message: { error in + Text(error.localizedDescription) + }) + .task { + await mastodonController.loadFilters() + } + } + + private func filtersSection(_ filters: [FilterMO]) -> some View { + Section { + ForEach(filters, id: \.id) { filter in + NavigationLink { + EditFilterView(filter: filter) { + updateFilter(filter) + } + } label: { + FilterRow(filter: filter) + } + .contextMenu { + Button(role: .destructive) { + deleteFilter(filter) + } label: { + Text("Delete Filter") + } + + } + } + .onDelete { indices in + for filter in indices.map({ filters[$0] }) { + deleteFilter(filter) + } + } + } + } + + private func deleteFilter(_ filter: FilterMO) { + Task { @MainActor in + let req = Filter.delete(filter.id) + do { + _ = try await mastodonController.run(req) + let context = mastodonController.persistentContainer.viewContext + context.delete(filter) + mastodonController.persistentContainer.save(context: context) + } catch { + self.deletionError = error + } + } + } + + private func updateFilter(_ filter: FilterMO) { + Task { @MainActor in + let req = Filter.update(filter) + do { + let (updated, _) = try await mastodonController.run(req) + filter.updateFrom(apiFilter: updated) + mastodonController.persistentContainer.save(context: mastodonController.persistentContainer.viewContext) + } catch { + self.updatingError = error + } + } + } +} + +//struct FiltersView_Previews: PreviewProvider { +// static var previews: some View { +// FiltersView() +// } +//} diff --git a/Tusker/Screens/Timeline/TimelinesPageViewController.swift b/Tusker/Screens/Timeline/TimelinesPageViewController.swift index 4219eccf..237edb14 100644 --- a/Tusker/Screens/Timeline/TimelinesPageViewController.swift +++ b/Tusker/Screens/Timeline/TimelinesPageViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import SwiftUI class TimelinesPageViewController: SegmentedPageViewController { @@ -40,6 +41,10 @@ class TimelinesPageViewController: SegmentedPageViewController { title = homeTitle tabBarItem.image = UIImage(systemName: "house.fill") + + let filtersItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), style: .plain, target: self, action: #selector(filtersPressed)) + filtersItem.accessibilityLabel = "Filters" + navigationItem.leftBarButtonItem = filtersItem } required init?(coder: NSCoder) { @@ -69,5 +74,9 @@ class TimelinesPageViewController: SegmentedPageViewController { let timelineVC = pageControllers[index] as! TimelineViewController timelineVC.restoreActivity(activity) } + + @objc private func filtersPressed() { + present(UIHostingController(rootView: FiltersView(mastodonController: mastodonController)), animated: true) + } }