V2 filters API, CoreData, and editing UI
This commit is contained in:
parent
518a8eba0a
commit
16a1e4008b
|
@ -229,12 +229,12 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Filters
|
// MARK: - Filters
|
||||||
public static func getFilters() -> Request<[Filter]> {
|
public static func getFiltersV1() -> Request<[FilterV1]> {
|
||||||
return Request<[Filter]>(method: .get, path: "/api/v1/filters")
|
return Request<[FilterV1]>(method: .get, path: "/api/v1/filters")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> {
|
public static func createFilterV1(phrase: String, context: [FilterV1.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<FilterV1> {
|
||||||
return Request<Filter>(method: .post, path: "/api/v1/filters", body: ParametersBody([
|
return Request<FilterV1>(method: .post, path: "/api/v1/filters", body: ParametersBody([
|
||||||
"phrase" => phrase,
|
"phrase" => phrase,
|
||||||
"irreversible" => irreversible,
|
"irreversible" => irreversible,
|
||||||
"whole_word" => wholeWord,
|
"whole_word" => wholeWord,
|
||||||
|
@ -242,8 +242,12 @@ public class Client {
|
||||||
] + "context" => context.contextStrings))
|
] + "context" => context.contextStrings))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getFilter(id: String) -> Request<Filter> {
|
public static func getFilterV1(id: String) -> Request<FilterV1> {
|
||||||
return Request<Filter>(method: .get, path: "/api/v1/filters/\(id)")
|
return Request<FilterV1>(method: .get, path: "/api/v1/filters/\(id)")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func getFiltersV2() -> Request<[FilterV2]> {
|
||||||
|
return Request(method: .get, path: "/api/v2/filters")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Follows
|
// MARK: - Follows
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// Filter.swift
|
// FilterV1.swift
|
||||||
// Pachyderm
|
// Pachyderm
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 9/9/18.
|
// Created by Shadowfacts on 9/9/18.
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Filter: FilterProtocol, Decodable {
|
public struct FilterV1: 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,13 +22,12 @@ public struct Filter: FilterProtocol, Decodable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func update(_ filter: some FilterProtocol, phrase: String? = nil, context: [Context]? = nil, irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresIn: TimeInterval? = nil) -> Request<Filter> {
|
public static func update(_ filterID: String, phrase: String, context: [Context], irreversible: Bool, wholeWord: Bool, expiresIn: TimeInterval?) -> Request<FilterV1> {
|
||||||
return Request<Filter>(method: .put, path: "/api/v1/filters/\(filter.id)", body: ParametersBody([
|
return Request<FilterV1>(method: .put, path: "/api/v1/filters/\(filterID)", body: ParametersBody([
|
||||||
"phrase" => (phrase ?? filter.phrase),
|
"phrase" => phrase,
|
||||||
"irreversible" => (irreversible ?? filter.irreversible),
|
"whole_word" => wholeWord,
|
||||||
"whole_word" => (wholeWord ?? filter.wholeWord),
|
"expires_in" => expiresIn,
|
||||||
"expires_in" => (expiresIn ?? filter.expiresAt?.timeIntervalSinceNow),
|
] + "context" => context.contextStrings))
|
||||||
] + "context" => (context ?? filter.contexts).contextStrings))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func delete(_ filterID: String) -> Request<Empty> {
|
public static func delete(_ filterID: String) -> Request<Empty> {
|
||||||
|
@ -45,7 +44,7 @@ public struct Filter: FilterProtocol, Decodable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Filter {
|
extension FilterV1 {
|
||||||
public enum Context: String, Decodable, CaseIterable {
|
public enum Context: String, Decodable, CaseIterable {
|
||||||
case home
|
case home
|
||||||
case notifications
|
case notifications
|
||||||
|
@ -55,7 +54,7 @@ extension Filter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Array where Element == Filter.Context {
|
extension Array where Element == FilterV1.Context {
|
||||||
var contextStrings: [String] {
|
var contextStrings: [String] {
|
||||||
return map { $0.rawValue }
|
return map { $0.rawValue }
|
||||||
}
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
//
|
||||||
|
// FilterV2.swift
|
||||||
|
// Pachyderm
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/2/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct FilterV2: Decodable {
|
||||||
|
public let id: String
|
||||||
|
public let title: String
|
||||||
|
public let context: [FilterV1.Context]
|
||||||
|
public let expiresAt: Date?
|
||||||
|
public let action: Action
|
||||||
|
public let keywords: [Keyword]
|
||||||
|
|
||||||
|
public static func update(
|
||||||
|
_ filterID: String,
|
||||||
|
title: String,
|
||||||
|
context: [FilterV1.Context],
|
||||||
|
expiresAt: Date?,
|
||||||
|
action: Action,
|
||||||
|
keywords keywordUpdates: [KeywordUpdate]
|
||||||
|
) -> Request<FilterV2> {
|
||||||
|
var keywordsParams = [Parameter]()
|
||||||
|
for (index, update) in keywordUpdates.enumerated() {
|
||||||
|
switch update {
|
||||||
|
case .update(id: let id, keyword: let keyword, wholeWord: let wholeWord):
|
||||||
|
keywordsParams.append("keywords_attributes[\(index)][id]" => id)
|
||||||
|
keywordsParams.append("keywords_attributes[\(index)][keyword]" => keyword)
|
||||||
|
keywordsParams.append("keywords_attributes[\(index)][whole_word]" => wholeWord)
|
||||||
|
case .add(keyword: let keyword, wholeWord: let wholeWord):
|
||||||
|
keywordsParams.append("keywords_attributes[\(index)][keyword]" => keyword)
|
||||||
|
keywordsParams.append("keywords_attributes[\(index)][whole_word]" => wholeWord)
|
||||||
|
case .destroy(id: let id):
|
||||||
|
keywordsParams.append("keywords_attributes[\(index)][id]" => id)
|
||||||
|
keywordsParams.append("keywords_attributes[\(index)][_destroy]" => true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Request(method: .put, path: "/api/v2/filters/\(filterID)", body: ParametersBody([
|
||||||
|
"title" => title,
|
||||||
|
"expires_at" => expiresAt,
|
||||||
|
"action" => action.rawValue,
|
||||||
|
] + "context" => context.contextStrings + keywordsParams))
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case title
|
||||||
|
case context
|
||||||
|
case expiresAt = "expires_at"
|
||||||
|
case action = "filter_action"
|
||||||
|
case keywords
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FilterV2 {
|
||||||
|
public enum Action: String, Decodable, Hashable, CaseIterable {
|
||||||
|
case warn
|
||||||
|
case hide
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FilterV2 {
|
||||||
|
public struct Keyword: Decodable {
|
||||||
|
public let id: String
|
||||||
|
public let keyword: String
|
||||||
|
public let wholeWord: Bool
|
||||||
|
|
||||||
|
public init(id: String, keyword: String, wholeWord: Bool) {
|
||||||
|
self.id = id
|
||||||
|
self.keyword = keyword
|
||||||
|
self.wholeWord = wholeWord
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case keyword
|
||||||
|
case wholeWord = "whole_word"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FilterV2 {
|
||||||
|
public enum KeywordUpdate {
|
||||||
|
case update(id: String, keyword: String, wholeWord: Bool)
|
||||||
|
case add(keyword: String, wholeWord: Bool)
|
||||||
|
case destroy(id: String)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
//
|
|
||||||
// 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 }
|
|
||||||
}
|
|
|
@ -58,7 +58,9 @@
|
||||||
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 */; };
|
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75A4293ABD6F00C0B37F /* EditFilterView.swift */; };
|
||||||
D61F75AD293AF39000C0B37F /* FilterContext+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75AC293AF39000C0B37F /* FilterContext+Helpers.swift */; };
|
D61F75AB293AF11400C0B37F /* FilterKeywordMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75AA293AF11400C0B37F /* FilterKeywordMO.swift */; };
|
||||||
|
D61F75AD293AF39000C0B37F /* Filter+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */; };
|
||||||
|
D61F75AF293AF50C00C0B37F /* EditedFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75AE293AF50C00C0B37F /* EditedFilter.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 */; };
|
||||||
|
@ -433,7 +435,9 @@
|
||||||
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>"; };
|
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>"; };
|
D61F75AA293AF11400C0B37F /* FilterKeywordMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterKeywordMO.swift; sourceTree = "<group>"; };
|
||||||
|
D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Filter+Helpers.swift"; sourceTree = "<group>"; };
|
||||||
|
D61F75AE293AF50C00C0B37F /* EditedFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedFilter.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>"; };
|
||||||
|
@ -789,6 +793,7 @@
|
||||||
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */,
|
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */,
|
||||||
D677284D24ECC01D00C732D3 /* Draft.swift */,
|
D677284D24ECC01D00C732D3 /* Draft.swift */,
|
||||||
D627FF75217E923E00CC0648 /* DraftsManager.swift */,
|
D627FF75217E923E00CC0648 /* DraftsManager.swift */,
|
||||||
|
D61F75AE293AF50C00C0B37F /* EditedFilter.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -911,6 +916,7 @@
|
||||||
D6B9366C2828444F00237D0E /* SavedInstance.swift */,
|
D6B9366C2828444F00237D0E /* SavedInstance.swift */,
|
||||||
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */,
|
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */,
|
||||||
D61F759A29384F9C00C0B37F /* FilterMO.swift */,
|
D61F759A29384F9C00C0B37F /* FilterMO.swift */,
|
||||||
|
D61F75AA293AF11400C0B37F /* FilterKeywordMO.swift */,
|
||||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
||||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
||||||
);
|
);
|
||||||
|
@ -1202,7 +1208,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 */,
|
D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1925,7 +1931,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 */,
|
D61F75AD293AF39000C0B37F /* Filter+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 */,
|
||||||
|
@ -1999,6 +2005,7 @@
|
||||||
D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */,
|
D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */,
|
||||||
D6BEA24B291C6A2B002F4D01 /* AlertWithData.swift in Sources */,
|
D6BEA24B291C6A2B002F4D01 /* AlertWithData.swift in Sources */,
|
||||||
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */,
|
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */,
|
||||||
|
D61F75AB293AF11400C0B37F /* FilterKeywordMO.swift in Sources */,
|
||||||
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
|
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
|
||||||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
||||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||||
|
@ -2059,6 +2066,7 @@
|
||||||
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */,
|
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */,
|
||||||
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */,
|
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */,
|
||||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
||||||
|
D61F75AF293AF50C00C0B37F /* EditedFilter.swift in Sources */,
|
||||||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||||
D6F6A5592920676800F496A8 /* ComposeToolbar.swift in Sources */,
|
D6F6A5592920676800F496A8 /* ComposeToolbar.swift in Sources */,
|
||||||
|
|
|
@ -51,11 +51,11 @@ struct InstanceFeatures {
|
||||||
}
|
}
|
||||||
|
|
||||||
var trendingStatusesAndLinks: Bool {
|
var trendingStatusesAndLinks: Bool {
|
||||||
instanceType.isMastodon && hasVersion(3, 5, 0)
|
instanceType.isMastodon && hasMastodonVersion(3, 5, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var reblogVisibility: Bool {
|
var reblogVisibility: Bool {
|
||||||
(instanceType.isMastodon && hasVersion(2, 8, 0))
|
(instanceType.isMastodon && hasMastodonVersion(2, 8, 0))
|
||||||
|| (instanceType.isPleroma && hasPleromaVersion(2, 0, 0))
|
|| (instanceType.isPleroma && hasPleromaVersion(2, 0, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,11 +85,11 @@ struct InstanceFeatures {
|
||||||
}
|
}
|
||||||
|
|
||||||
var canFollowHashtags: Bool {
|
var canFollowHashtags: Bool {
|
||||||
if case .mastodon(_, .some(let version)) = instanceType {
|
hasMastodonVersion(4, 0, 0)
|
||||||
return version >= Version(4, 0, 0)
|
}
|
||||||
} else {
|
|
||||||
return false
|
var filtersV2: Bool {
|
||||||
}
|
hasMastodonVersion(4, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func update(instance: Instance, nodeInfo: NodeInfo?) {
|
mutating func update(instance: Instance, nodeInfo: NodeInfo?) {
|
||||||
|
@ -138,7 +138,7 @@ struct InstanceFeatures {
|
||||||
maxStatusChars = instance.maxStatusCharacters ?? 500
|
maxStatusChars = instance.maxStatusCharacters ?? 500
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
|
func hasMastodonVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
|
||||||
if case .mastodon(_, .some(let version)) = instanceType {
|
if case .mastodon(_, .some(let version)) = instanceType {
|
||||||
return version >= Version(major, minor, patch)
|
return version >= Version(major, minor, patch)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -356,9 +356,20 @@ class MastodonController: ObservableObject {
|
||||||
func loadFilters() async {
|
func loadFilters() async {
|
||||||
filters = (try? persistentContainer.viewContext.fetch(FilterMO.fetchRequest())) ?? []
|
filters = (try? persistentContainer.viewContext.fetch(FilterMO.fetchRequest())) ?? []
|
||||||
|
|
||||||
let req = Client.getFilters()
|
var apiFilters: [AnyFilter]?
|
||||||
if let (filters, _) = try? await run(req) {
|
if instanceFeatures.filtersV2 {
|
||||||
self.persistentContainer.updateFilters(filters) {
|
let req = Client.getFiltersV2()
|
||||||
|
if let (filters, _) = try? await run(req) {
|
||||||
|
apiFilters = filters.map { .v2($0) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let req = Client.getFiltersV1()
|
||||||
|
if let (filters, _) = try? await run(req) {
|
||||||
|
apiFilters = filters.map { .v1($0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let apiFilters {
|
||||||
|
self.persistentContainer.updateFilters(apiFilters) {
|
||||||
if case .success(let filters) = $0 {
|
if case .success(let filters) = $0 {
|
||||||
self.filters = filters
|
self.filters = filters
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
//
|
||||||
|
// FilterKeywordMO.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/2/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
@objc(FilterKeywordMO)
|
||||||
|
public final class FilterKeywordMO: NSManagedObject {
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<FilterKeywordMO> {
|
||||||
|
return NSFetchRequest(entityName: "FilterKeyword")
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged public var id: String?
|
||||||
|
@NSManaged public var keyword: String
|
||||||
|
@NSManaged public var wholeWord: Bool
|
||||||
|
@NSManaged public var filter: FilterMO
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FilterKeywordMO {
|
||||||
|
convenience init(apiKeyword keyword: FilterV2.Keyword, context: NSManagedObjectContext) {
|
||||||
|
self.init(context: context)
|
||||||
|
self.updateFrom(apiKeyword: keyword)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFrom(apiKeyword keyword: FilterV2.Keyword) {
|
||||||
|
self.id = keyword.id
|
||||||
|
self.keyword = keyword.keyword
|
||||||
|
self.wholeWord = keyword.wholeWord
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,21 +11,20 @@ import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
@objc(FilterMO)
|
@objc(FilterMO)
|
||||||
public final class FilterMO: NSManagedObject, FilterProtocol {
|
public final class FilterMO: NSManagedObject {
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<FilterMO> {
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<FilterMO> {
|
||||||
return NSFetchRequest(entityName: "Filter")
|
return NSFetchRequest(entityName: "Filter")
|
||||||
}
|
}
|
||||||
|
|
||||||
@NSManaged public var id: String
|
@NSManaged public var id: String
|
||||||
@NSManaged public var phrase: String
|
@NSManaged public var title: String?
|
||||||
@NSManaged private var context: String
|
@NSManaged private var context: String
|
||||||
@NSManaged public var expiresAt: Date?
|
@NSManaged public var expiresAt: Date?
|
||||||
@NSManaged public var irreversible: Bool
|
@NSManaged public var keywords: NSMutableSet
|
||||||
@NSManaged public var wholeWord: Bool
|
@NSManaged public var action: String
|
||||||
|
|
||||||
private var _contexts: [Filter.Context]?
|
private var _contexts: [FilterV1.Context]?
|
||||||
public var contexts: [Filter.Context] {
|
public var contexts: [FilterV1.Context] {
|
||||||
get {
|
get {
|
||||||
if let _contexts {
|
if let _contexts {
|
||||||
return _contexts
|
return _contexts
|
||||||
|
@ -40,6 +39,23 @@ public final class FilterMO: NSManagedObject, FilterProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var keywordMOs: [FilterKeywordMO] {
|
||||||
|
keywords.allObjects as! [FilterKeywordMO]
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleOrKeyword: String {
|
||||||
|
title ?? (keywords.allObjects.first! as! FilterKeywordMO).keyword
|
||||||
|
}
|
||||||
|
|
||||||
|
var filterAction: FilterV2.Action {
|
||||||
|
get {
|
||||||
|
FilterV2.Action(rawValue: action)!
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
action = newValue.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override func didChangeValue(forKey key: String) {
|
public override func didChangeValue(forKey key: String) {
|
||||||
super.didChangeValue(forKey: key)
|
super.didChangeValue(forKey: key)
|
||||||
if key == "context" {
|
if key == "context" {
|
||||||
|
@ -50,17 +66,69 @@ public final class FilterMO: NSManagedObject, FilterProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FilterMO {
|
extension FilterMO {
|
||||||
convenience init(apiFilter filter: Filter, context: NSManagedObjectContext) {
|
func updateFrom(apiFilter filter: AnyFilter, context: NSManagedObjectContext) {
|
||||||
self.init(context: context)
|
switch filter {
|
||||||
self.updateFrom(apiFilter: filter)
|
case .v1(let v1):
|
||||||
|
self.updateFrom(apiFilter: v1, context: context)
|
||||||
|
case .v2(let v2):
|
||||||
|
self.updateFrom(apiFilter: v2, context: context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateFrom(apiFilter filter: Filter) {
|
func updateFrom(apiFilter filter: FilterV1, context: NSManagedObjectContext) {
|
||||||
self.id = filter.id
|
self.id = filter.id
|
||||||
self.phrase = filter.phrase
|
self.title = nil
|
||||||
self.contexts = filter.contexts
|
self.contexts = filter.contexts
|
||||||
self.expiresAt = filter.expiresAt
|
self.expiresAt = filter.expiresAt
|
||||||
self.irreversible = filter.irreversible
|
self.filterAction = .warn
|
||||||
self.wholeWord = filter.wholeWord
|
let keyword: FilterKeywordMO
|
||||||
|
if self.keywords.count == 0 {
|
||||||
|
keyword = FilterKeywordMO(context: context)
|
||||||
|
keyword.filter = self
|
||||||
|
self.keywords.add(keyword)
|
||||||
|
} else {
|
||||||
|
keyword = self.keywords.allObjects.first! as! FilterKeywordMO
|
||||||
|
}
|
||||||
|
keyword.keyword = filter.phrase
|
||||||
|
keyword.wholeWord = filter.wholeWord
|
||||||
|
keyword.id = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFrom(apiFilter filter: FilterV2, context: NSManagedObjectContext) {
|
||||||
|
self.id = filter.id
|
||||||
|
self.title = filter.title
|
||||||
|
self.contexts = filter.context
|
||||||
|
self.expiresAt = filter.expiresAt
|
||||||
|
self.filterAction = filter.action
|
||||||
|
|
||||||
|
var existing = keywordMOs
|
||||||
|
for keyword in filter.keywords {
|
||||||
|
if let mo = existing.first(where: { $0.id == keyword.id }) {
|
||||||
|
mo.updateFrom(apiKeyword: keyword)
|
||||||
|
existing.removeAll(where: { $0.id == keyword.id })
|
||||||
|
} else {
|
||||||
|
let mo = FilterKeywordMO(context: context)
|
||||||
|
mo.updateFrom(apiKeyword: keyword)
|
||||||
|
mo.filter = self
|
||||||
|
self.keywords.add(mo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for unupdated in existing {
|
||||||
|
context.delete(unupdated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AnyFilter {
|
||||||
|
case v1(FilterV1)
|
||||||
|
case v2(FilterV2)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .v1(let v1):
|
||||||
|
return v1.id
|
||||||
|
case .v2(let v2):
|
||||||
|
return v2.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -299,7 +299,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateFilters(_ filters: [Filter], completion: @escaping (Result<[FilterMO], Error>) -> Void) {
|
func updateFilters(_ filters: [AnyFilter], completion: @escaping (Result<[FilterMO], Error>) -> Void) {
|
||||||
viewContext.perform {
|
viewContext.perform {
|
||||||
do {
|
do {
|
||||||
var all = try self.viewContext.fetch(FilterMO.fetchRequest())
|
var all = try self.viewContext.fetch(FilterMO.fetchRequest())
|
||||||
|
@ -309,9 +309,14 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
try self.viewContext.execute(NSBatchDeleteRequest(objectIDs: toDelete))
|
try self.viewContext.execute(NSBatchDeleteRequest(objectIDs: toDelete))
|
||||||
}
|
}
|
||||||
|
|
||||||
for filter in filters where !all.contains(where: { $0.id == filter.id }) {
|
for filter in filters {
|
||||||
let mo = FilterMO(apiFilter: filter, context: self.viewContext)
|
if let existing = all.first(where: { $0.id == filter.id }) {
|
||||||
all.append(mo)
|
existing.updateFrom(apiFilter: filter, context: self.viewContext)
|
||||||
|
} else {
|
||||||
|
let mo = FilterMO(context: self.viewContext)
|
||||||
|
mo.updateFrom(apiFilter: filter, context: self.viewContext)
|
||||||
|
all.append(mo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save(context: self.viewContext)
|
self.save(context: self.viewContext)
|
||||||
|
|
|
@ -29,12 +29,18 @@
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Filter" representedClassName="FilterMO" syncable="YES">
|
<entity name="Filter" representedClassName="FilterMO" syncable="YES">
|
||||||
|
<attribute name="action" attributeType="String" defaultValueString="warn"/>
|
||||||
<attribute name="context" attributeType="String"/>
|
<attribute name="context" attributeType="String"/>
|
||||||
<attribute name="expiresAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="expiresAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="id" attributeType="String"/>
|
<attribute name="id" attributeType="String"/>
|
||||||
<attribute name="irreversible" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="title" optional="YES" attributeType="String"/>
|
||||||
<attribute name="phrase" attributeType="String"/>
|
<relationship name="keywords" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="FilterKeyword" inverseName="filter" inverseEntity="FilterKeyword"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="FilterKeyword" representedClassName="FilterKeywordMO" syncable="YES">
|
||||||
|
<attribute name="id" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="keyword" attributeType="String"/>
|
||||||
<attribute name="wholeWord" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="wholeWord" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="filter" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Filter" inverseName="keywords" inverseEntity="Filter"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="FollowedHashtag" representedClassName="FollowedHashtag" syncable="YES">
|
<entity name="FollowedHashtag" representedClassName="FollowedHashtag" syncable="YES">
|
||||||
<attribute name="name" attributeType="String"/>
|
<attribute name="name" attributeType="String"/>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// FilterContext+Helpers.swift
|
// Filter+Helpers.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 12/2/22.
|
// Created by Shadowfacts on 12/2/22.
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
extension Filter.Context {
|
extension FilterV1.Context {
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .home:
|
case .home:
|
||||||
|
@ -24,3 +24,14 @@ extension Filter.Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension FilterV2.Action {
|
||||||
|
var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .warn:
|
||||||
|
return "Warn"
|
||||||
|
case .hide:
|
||||||
|
return "Hide"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
//
|
||||||
|
// EditedFilter.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/2/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class EditedFilter: ObservableObject {
|
||||||
|
let id: String?
|
||||||
|
@Published var title: String?
|
||||||
|
@Published var contexts: [FilterV1.Context]
|
||||||
|
@Published var expiresIn: TimeInterval?
|
||||||
|
@Published var keywords: [Keyword]
|
||||||
|
@Published var action: FilterV2.Action
|
||||||
|
|
||||||
|
init(_ mo: FilterMO) {
|
||||||
|
self.id = mo.id
|
||||||
|
self.title = mo.title
|
||||||
|
self.contexts = mo.contexts
|
||||||
|
if let expiresAt = mo.expiresAt {
|
||||||
|
expiresIn = expiresAt.timeIntervalSinceNow
|
||||||
|
}
|
||||||
|
self.keywords = mo.keywordMOs.map {
|
||||||
|
Keyword(id: $0.id, keyword: $0.keyword, wholeWord: $0.wholeWord)
|
||||||
|
}
|
||||||
|
self.action = mo.filterAction
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Keyword {
|
||||||
|
let id: String?
|
||||||
|
var keyword: String
|
||||||
|
var wholeWord: Bool
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,19 +27,18 @@ struct EditFilterView: View {
|
||||||
return durations.map { .init(value: $0, title: f.string(from: $0)!) }
|
return durations.map { .init(value: $0, title: f.string(from: $0)!) }
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ObservedObject var filter: FilterMO
|
@ObservedObject var filter: EditedFilter
|
||||||
let updateFilter: () -> Void
|
let updateFilter: (EditedFilter) -> Void
|
||||||
@EnvironmentObject private var mastodonController: MastodonController
|
@EnvironmentObject private var mastodonController: MastodonController
|
||||||
@State private var edited = false
|
@State private var edited = false
|
||||||
|
|
||||||
init(filter: FilterMO, updateFilter: @escaping () -> Void) {
|
init(filter: EditedFilter, updateFilter: @escaping (EditedFilter) -> Void) {
|
||||||
self.filter = filter
|
self.filter = filter
|
||||||
self.updateFilter = updateFilter
|
self.updateFilter = updateFilter
|
||||||
if let expiresAt = filter.expiresAt {
|
if let expiresIn = filter.expiresIn {
|
||||||
let dist = expiresAt.timeIntervalSinceNow
|
|
||||||
self.expiresIn = Self.expiresInOptions.min(by: { a, b in
|
self.expiresIn = Self.expiresInOptions.min(by: { a, b in
|
||||||
let aDist = abs(a.value - dist)
|
let aDist = abs(a.value - expiresIn)
|
||||||
let bDist = abs(b.value - dist)
|
let bDist = abs(b.value - expiresIn)
|
||||||
return aDist < bDist
|
return aDist < bDist
|
||||||
})!.value
|
})!.value
|
||||||
} else {
|
} else {
|
||||||
|
@ -49,32 +48,68 @@ struct EditFilterView: View {
|
||||||
|
|
||||||
@State private var expiresIn: TimeInterval {
|
@State private var expiresIn: TimeInterval {
|
||||||
didSet {
|
didSet {
|
||||||
if filter.expiresAt != nil {
|
if expires.wrappedValue {
|
||||||
filter.expiresAt = Date(timeIntervalSinceNow: expiresIn)
|
filter.expiresIn = expiresIn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var expires: Binding<Bool> {
|
private var expires: Binding<Bool> {
|
||||||
Binding {
|
Binding {
|
||||||
filter.expiresAt != nil
|
filter.expiresIn != nil
|
||||||
} set: { newValue in
|
} set: { newValue in
|
||||||
if newValue {
|
filter.expiresIn = newValue ? expiresIn : nil
|
||||||
filter.expiresAt = Date(timeIntervalSinceNow: expiresIn)
|
|
||||||
} else {
|
|
||||||
filter.expiresAt = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
if mastodonController.instanceFeatures.filtersV2 {
|
||||||
TextField("Phrase", text: $filter.phrase)
|
Section {
|
||||||
Toggle("Whole Word", isOn: $filter.wholeWord)
|
TextField("Title", text: Binding(get: {
|
||||||
|
filter.title ?? ""
|
||||||
|
}, set: { newValue in
|
||||||
|
filter.title = newValue
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
ForEach(Array($filter.keywords.enumerated()), id: \.offset) { keyword in
|
||||||
|
VStack {
|
||||||
|
TextField("Phrase", text: keyword.element.keyword)
|
||||||
|
Toggle("Whole Word", isOn: keyword.element.wholeWord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete(perform: mastodonController.instanceFeatures.filtersV2 ? { indices in
|
||||||
|
filter.keywords.remove(atOffsets: indices)
|
||||||
|
} : nil)
|
||||||
|
|
||||||
|
if mastodonController.instanceFeatures.filtersV2 {
|
||||||
|
Button {
|
||||||
|
let new = EditedFilter.Keyword(id: nil, keyword: "", wholeWord: true)
|
||||||
|
withAnimation {
|
||||||
|
filter.keywords.append(new)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Add Keyword", systemImage: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
if mastodonController.instanceFeatures.filtersV2 {
|
||||||
|
Picker(selection: $filter.action) {
|
||||||
|
ForEach(FilterV2.Action.allCases, id: \.self) { action in
|
||||||
|
Text(action.displayName).tag(action)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Toggle("Expires", isOn: expires)
|
Toggle("Expires", isOn: expires)
|
||||||
|
|
||||||
if expires.wrappedValue {
|
if expires.wrappedValue {
|
||||||
Picker(selection: $expiresIn) {
|
Picker(selection: $expiresIn) {
|
||||||
ForEach(Self.expiresInOptions, id: \.value) { option in
|
ForEach(Self.expiresInOptions, id: \.value) { option in
|
||||||
|
@ -87,7 +122,7 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
ForEach(Filter.Context.allCases, id: \.rawValue) { context in
|
ForEach(FilterV1.Context.allCases, id: \.rawValue) { context in
|
||||||
Toggle(isOn: Binding(get: {
|
Toggle(isOn: Binding(get: {
|
||||||
filter.contexts.contains(context)
|
filter.contexts.contains(context)
|
||||||
}, set: { newValue in
|
}, set: { newValue in
|
||||||
|
@ -114,7 +149,7 @@ struct EditFilterView: View {
|
||||||
})
|
})
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
if edited {
|
if edited {
|
||||||
updateFilter()
|
updateFilter(filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,12 @@ import Pachyderm
|
||||||
|
|
||||||
struct FilterRow: View {
|
struct FilterRow: View {
|
||||||
@ObservedObject var filter: FilterMO
|
@ObservedObject var filter: FilterMO
|
||||||
|
@EnvironmentObject var mastodonController: MastodonController
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
Text(filter.phrase)
|
Text(mastodonController.instanceFeatures.filtersV2 ? filter.title ?? "" : filter.keywordMOs.first!.keyword)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -33,10 +34,10 @@ struct FilterRow: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
// rather than mapping over filter.contexts, because we want a consistent order
|
// rather than mapping over filter.contexts, because we want a consistent order
|
||||||
Text(Filter.Context.allCases.filter { filter.contexts.contains($0) }.map(\.displayName).formatted())
|
Text(FilterV1.Context.allCases.filter { filter.contexts.contains($0) }.map(\.displayName).formatted())
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
|
||||||
if filter.wholeWord {
|
if !mastodonController.instanceFeatures.filtersV2 && filter.keywordMOs.first!.wholeWord {
|
||||||
Text("Whole word")
|
Text("Whole word")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
@ -49,10 +50,9 @@ struct FilterRow_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let filter = FilterMO()
|
let filter = FilterMO()
|
||||||
filter.id = "1"
|
filter.id = "1"
|
||||||
filter.phrase = "test"
|
// filter.phrase = "test"
|
||||||
filter.expiresAt = Date().addingTimeInterval(60 * 60)
|
filter.expiresAt = Date().addingTimeInterval(60 * 60)
|
||||||
filter.wholeWord = true
|
// filter.wholeWord = true
|
||||||
filter.irreversible = false
|
|
||||||
filter.contexts = [.home]
|
filter.contexts = [.home]
|
||||||
return FilterRow(filter: filter)
|
return FilterRow(filter: filter)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,14 +13,15 @@ struct FiltersView: View {
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
FiltersList(mastodonController: mastodonController)
|
FiltersList()
|
||||||
|
.environmentObject(mastodonController)
|
||||||
.environment(\.managedObjectContext, mastodonController.persistentContainer.viewContext)
|
.environment(\.managedObjectContext, mastodonController.persistentContainer.viewContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FiltersList: View {
|
struct FiltersList: View {
|
||||||
let mastodonController: MastodonController
|
@EnvironmentObject private var mastodonController: MastodonController
|
||||||
@FetchRequest(sortDescriptors: []) private var filters: FetchedResults<FilterMO>
|
@FetchRequest(sortDescriptors: []) private var filters: FetchedResults<FilterMO>
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var deletionError: (any Error)?
|
@State private var deletionError: (any Error)?
|
||||||
|
@ -40,11 +41,11 @@ struct FiltersList: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var unexpiredFilters: [FilterMO] {
|
private var unexpiredFilters: [FilterMO] {
|
||||||
filters.filter { $0.expiresAt == nil || $0.expiresAt! > Date() }.sorted(using: SemiCaseSensitiveComparator.keyPath(\.phrase))
|
filters.filter { $0.expiresAt == nil || $0.expiresAt! > Date() }.sorted(using: SemiCaseSensitiveComparator.keyPath(\.titleOrKeyword))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var expiredFilters: [FilterMO] {
|
private var expiredFilters: [FilterMO] {
|
||||||
filters.filter { $0.expiresAt != nil && $0.expiresAt! <= Date() }.sorted(using: SemiCaseSensitiveComparator.keyPath(\.phrase))
|
filters.filter { $0.expiresAt != nil && $0.expiresAt! <= Date() }.sorted(using: SemiCaseSensitiveComparator.keyPath(\.titleOrKeyword))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var navigationBody: some View {
|
private var navigationBody: some View {
|
||||||
|
@ -84,9 +85,7 @@ struct FiltersList: View {
|
||||||
Section {
|
Section {
|
||||||
ForEach(filters, id: \.id) { filter in
|
ForEach(filters, id: \.id) { filter in
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
EditFilterView(filter: filter) {
|
EditFilterView(filter: EditedFilter(filter), updateFilter: updateFilter)
|
||||||
updateFilter(filter)
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
FilterRow(filter: filter)
|
FilterRow(filter: filter)
|
||||||
}
|
}
|
||||||
|
@ -109,7 +108,7 @@ struct FiltersList: View {
|
||||||
|
|
||||||
private func deleteFilter(_ filter: FilterMO) {
|
private func deleteFilter(_ filter: FilterMO) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
let req = Filter.delete(filter.id)
|
let req = FilterV1.delete(filter.id)
|
||||||
do {
|
do {
|
||||||
_ = try await mastodonController.run(req)
|
_ = try await mastodonController.run(req)
|
||||||
let context = mastodonController.persistentContainer.viewContext
|
let context = mastodonController.persistentContainer.viewContext
|
||||||
|
@ -121,13 +120,41 @@ struct FiltersList: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateFilter(_ filter: FilterMO) {
|
private func updateFilter(_ filter: EditedFilter) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
let req = Filter.update(filter)
|
|
||||||
do {
|
do {
|
||||||
let (updated, _) = try await mastodonController.run(req)
|
let mo = filters.first(where: { $0.id == filter.id })!
|
||||||
filter.updateFrom(apiFilter: updated)
|
|
||||||
mastodonController.persistentContainer.save(context: mastodonController.persistentContainer.viewContext)
|
let updateFrom: AnyFilter
|
||||||
|
if mastodonController.instanceFeatures.filtersV2 {
|
||||||
|
var expiresAt: Date?
|
||||||
|
if let expiresIn = filter.expiresIn {
|
||||||
|
expiresAt = Date(timeIntervalSinceNow: expiresIn)
|
||||||
|
}
|
||||||
|
var updates = filter.keywords.map {
|
||||||
|
if let id = $0.id {
|
||||||
|
return FilterV2.KeywordUpdate.update(id: id, keyword: $0.keyword, wholeWord: $0.wholeWord)
|
||||||
|
} else {
|
||||||
|
return FilterV2.KeywordUpdate.add(keyword: $0.keyword, wholeWord: $0.wholeWord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for existing in mo.keywordMOs where !filter.keywords.contains(where: { existing.id == $0.id }) {
|
||||||
|
if let id = existing.id {
|
||||||
|
updates.append(.destroy(id: id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let req = FilterV2.update(filter.id!, title: filter.title ?? "", context: filter.contexts, expiresAt: expiresAt, action: filter.action, keywords: updates)
|
||||||
|
let (updated, _) = try await mastodonController.run(req)
|
||||||
|
updateFrom = .v2(updated)
|
||||||
|
} else {
|
||||||
|
let req = FilterV1.update(filter.id!, phrase: filter.keywords.first!.keyword, context: filter.contexts, irreversible: false, wholeWord: filter.keywords.first!.wholeWord, expiresIn: filter.expiresIn)
|
||||||
|
let (updated, _) = try await mastodonController.run(req)
|
||||||
|
updateFrom = .v1(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = mastodonController.persistentContainer.viewContext
|
||||||
|
mo.updateFrom(apiFilter: updateFrom, context: context)
|
||||||
|
mastodonController.persistentContainer.save(context: context)
|
||||||
} catch {
|
} catch {
|
||||||
self.updatingError = error
|
self.updatingError = error
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,10 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(sceneWillEnterForeground), name: UIScene.willEnterForegroundNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(sceneWillEnterForeground), name: UIScene.willEnterForegroundNotification, object: nil)
|
||||||
|
|
||||||
|
mastodonController.run(Client.getFiltersV2()) { response in
|
||||||
|
print(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// separate method because InstanceTimelineViewController needs to be able to customize it
|
// separate method because InstanceTimelineViewController needs to be able to customize it
|
||||||
|
|
Loading…
Reference in New Issue