forked from shadowfacts/Tusker
Start doing filters UI
This commit is contained in:
parent
8d56a6450e
commit
518a8eba0a
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Filter: Decodable {
|
public struct Filter: FilterProtocol, Decodable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let phrase: String
|
public let phrase: String
|
||||||
private let context: [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<Filter> {
|
public static func update(_ filter: some FilterProtocol, phrase: String? = nil, context: [Context]? = nil, irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresIn: TimeInterval? = nil) -> Request<Filter> {
|
||||||
return Request<Filter>(method: .put, path: "/api/v1/filters/\(filter.id)", body: ParametersBody([
|
return Request<Filter>(method: .put, path: "/api/v1/filters/\(filter.id)", body: ParametersBody([
|
||||||
"phrase" => (phrase ?? filter.phrase),
|
"phrase" => (phrase ?? filter.phrase),
|
||||||
"irreversible" => (irreversible ?? filter.irreversible),
|
"irreversible" => (irreversible ?? filter.irreversible),
|
||||||
"whole_word" => (wholeWord ?? filter.wholeWord),
|
"whole_word" => (wholeWord ?? filter.wholeWord),
|
||||||
"expires_at" => (expiresAt ?? filter.expiresAt)
|
"expires_in" => (expiresIn ?? filter.expiresAt?.timeIntervalSinceNow),
|
||||||
] + "context" => (context?.contextStrings ?? filter.context)))
|
] + "context" => (context ?? filter.contexts).contextStrings))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func delete(_ filter: Filter) -> Request<Empty> {
|
public static func delete(_ filterID: String) -> Request<Empty> {
|
||||||
return Request<Empty>(method: .delete, path: "/api/v1/filters/\(filter.id)")
|
return Request<Empty>(method: .delete, path: "/api/v1/filters/\(filterID)")
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
@ -46,7 +46,7 @@ public struct Filter: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Filter {
|
extension Filter {
|
||||||
public enum Context: String, Decodable {
|
public enum Context: String, Decodable, CaseIterable {
|
||||||
case home
|
case home
|
||||||
case notifications
|
case notifications
|
||||||
case `public`
|
case `public`
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
|
@ -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 {
|
static func =>(name: String, focus: (Float, Float)?) -> Parameter {
|
||||||
guard let focus = focus else { return Parameter(name: name, value: nil) }
|
guard let focus = focus else { return Parameter(name: name, value: nil) }
|
||||||
return Parameter(name: name, value: "\(focus.0),\(focus.1)")
|
return Parameter(name: name, value: "\(focus.0),\(focus.1)")
|
||||||
|
|
|
@ -52,9 +52,13 @@
|
||||||
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; };
|
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; };
|
||||||
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; };
|
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; };
|
||||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75952937037800C0B37F /* ToggleFollowHashtagService.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 */; };
|
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 */; };
|
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */; };
|
||||||
D61F75A129396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.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 */; };
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.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 = "<group>"; };
|
D61F759129365C6C00C0B37F /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedHashtag.swift; sourceTree = "<group>"; };
|
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedHashtag.swift; sourceTree = "<group>"; };
|
||||||
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleFollowHashtagService.swift; sourceTree = "<group>"; };
|
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleFollowHashtagService.swift; sourceTree = "<group>"; };
|
||||||
|
D61F759829384D4D00C0B37F /* FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersView.swift; sourceTree = "<group>"; };
|
||||||
D61F759A29384F9C00C0B37F /* FilterMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterMO.swift; sourceTree = "<group>"; };
|
D61F759A29384F9C00C0B37F /* FilterMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterMO.swift; sourceTree = "<group>"; };
|
||||||
|
D61F759C2938574B00C0B37F /* FilterRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterRow.swift; sourceTree = "<group>"; };
|
||||||
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiCaseSensitiveComparator.swift; sourceTree = "<group>"; };
|
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiCaseSensitiveComparator.swift; sourceTree = "<group>"; };
|
||||||
D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiCaseSensitiveComparatorTests.swift; sourceTree = "<group>"; };
|
D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiCaseSensitiveComparatorTests.swift; sourceTree = "<group>"; };
|
||||||
|
D61F75A4293ABD6F00C0B37F /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = "<group>"; };
|
||||||
|
D61F75AC293AF39000C0B37F /* FilterContext+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilterContext+Helpers.swift"; sourceTree = "<group>"; };
|
||||||
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -794,6 +802,16 @@
|
||||||
path = "Instance Cell";
|
path = "Instance Cell";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D61F759729384D4200C0B37F /* Filters */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D61F759829384D4D00C0B37F /* FiltersView.swift */,
|
||||||
|
D61F759C2938574B00C0B37F /* FilterRow.swift */,
|
||||||
|
D61F75A4293ABD6F00C0B37F /* EditFilterView.swift */,
|
||||||
|
);
|
||||||
|
path = Filters;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D623A53B2635F4E20095BD04 /* Poll */ = {
|
D623A53B2635F4E20095BD04 /* Poll */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -922,6 +940,7 @@
|
||||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||||
D627943C23A5635D00D38C68 /* Explore */,
|
D627943C23A5635D00D38C68 /* Explore */,
|
||||||
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
||||||
|
D61F759729384D4200C0B37F /* Filters */,
|
||||||
D641C788213DD86D004B4513 /* Large Image */,
|
D641C788213DD86D004B4513 /* Large Image */,
|
||||||
D627944B23A9A02400D38C68 /* Lists */,
|
D627944B23A9A02400D38C68 /* Lists */,
|
||||||
D641C782213DD7F0004B4513 /* Main */,
|
D641C782213DD7F0004B4513 /* Main */,
|
||||||
|
@ -1183,6 +1202,7 @@
|
||||||
D6ADB6ED28EA74E8009924AB /* UIView+Configure.swift */,
|
D6ADB6ED28EA74E8009924AB /* UIView+Configure.swift */,
|
||||||
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */,
|
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */,
|
||||||
D61F758F29353B4300C0B37F /* FileManager+Size.swift */,
|
D61F758F29353B4300C0B37F /* FileManager+Size.swift */,
|
||||||
|
D61F75AC293AF39000C0B37F /* FilterContext+Helpers.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1821,6 +1841,7 @@
|
||||||
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */,
|
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */,
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
||||||
|
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
||||||
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */,
|
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */,
|
||||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||||
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
|
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
|
||||||
|
@ -1876,6 +1897,7 @@
|
||||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
||||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||||
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
||||||
|
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
|
||||||
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
|
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
|
||||||
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */,
|
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */,
|
||||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||||
|
@ -1903,6 +1925,7 @@
|
||||||
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
|
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||||
D6BEA249291C6118002F4D01 /* DraftsView.swift in Sources */,
|
D6BEA249291C6118002F4D01 /* DraftsView.swift in Sources */,
|
||||||
|
D61F75AD293AF39000C0B37F /* FilterContext+Helpers.swift in Sources */,
|
||||||
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */,
|
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */,
|
||||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
||||||
D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */,
|
D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */,
|
||||||
|
@ -2012,6 +2035,7 @@
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
||||||
|
D61F759929384D4D00C0B37F /* FiltersView.swift in Sources */,
|
||||||
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */,
|
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */,
|
||||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||||
|
|
|
@ -11,7 +11,7 @@ import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
@objc(FilterMO)
|
@objc(FilterMO)
|
||||||
public final class FilterMO: NSManagedObject {
|
public final class FilterMO: NSManagedObject, FilterProtocol {
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<FilterMO> {
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<FilterMO> {
|
||||||
return NSFetchRequest(entityName: "Filter")
|
return NSFetchRequest(entityName: "Filter")
|
||||||
|
@ -25,7 +25,7 @@ public final class FilterMO: NSManagedObject {
|
||||||
@NSManaged public var wholeWord: Bool
|
@NSManaged public var wholeWord: Bool
|
||||||
|
|
||||||
private var _contexts: [Filter.Context]?
|
private var _contexts: [Filter.Context]?
|
||||||
var contexts: [Filter.Context] {
|
public var contexts: [Filter.Context] {
|
||||||
get {
|
get {
|
||||||
if let _contexts {
|
if let _contexts {
|
||||||
return _contexts
|
return _contexts
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<TimeInterval>.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<Bool> {
|
||||||
|
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()
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<FilterMO>
|
||||||
|
@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()
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -7,6 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
class TimelinesPageViewController: SegmentedPageViewController {
|
class TimelinesPageViewController: SegmentedPageViewController {
|
||||||
|
|
||||||
|
@ -40,6 +41,10 @@ class TimelinesPageViewController: SegmentedPageViewController {
|
||||||
|
|
||||||
title = homeTitle
|
title = homeTitle
|
||||||
tabBarItem.image = UIImage(systemName: "house.fill")
|
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) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -69,5 +74,9 @@ class TimelinesPageViewController: SegmentedPageViewController {
|
||||||
let timelineVC = pageControllers[index] as! TimelineViewController
|
let timelineVC = pageControllers[index] as! TimelineViewController
|
||||||
timelineVC.restoreActivity(activity)
|
timelineVC.restoreActivity(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func filtersPressed() {
|
||||||
|
present(UIHostingController(rootView: FiltersView(mastodonController: mastodonController)), animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue