Creating filters UI

This commit is contained in:
Shadowfacts 2022-12-03 14:17:10 -05:00
parent 16a1e4008b
commit 83ca7f1321
6 changed files with 158 additions and 51 deletions

View File

@ -233,12 +233,12 @@ public class Client {
return Request<[FilterV1]>(method: .get, path: "/api/v1/filters") return Request<[FilterV1]>(method: .get, path: "/api/v1/filters")
} }
public static func createFilterV1(phrase: String, context: [FilterV1.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<FilterV1> { public static func createFilterV1(phrase: String, context: [FilterV1.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresIn: TimeInterval? = nil) -> Request<FilterV1> {
return Request<FilterV1>(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,
"expires_at" => expiresAt "expires_in" => expiresIn,
] + "context" => context.contextStrings)) ] + "context" => context.contextStrings))
} }

View File

@ -19,7 +19,7 @@ public struct FilterV2: Decodable {
_ filterID: String, _ filterID: String,
title: String, title: String,
context: [FilterV1.Context], context: [FilterV1.Context],
expiresAt: Date?, expiresIn: TimeInterval?,
action: Action, action: Action,
keywords keywordUpdates: [KeywordUpdate] keywords keywordUpdates: [KeywordUpdate]
) -> Request<FilterV2> { ) -> Request<FilterV2> {
@ -40,7 +40,31 @@ public struct FilterV2: Decodable {
} }
return Request(method: .put, path: "/api/v2/filters/\(filterID)", body: ParametersBody([ return Request(method: .put, path: "/api/v2/filters/\(filterID)", body: ParametersBody([
"title" => title, "title" => title,
"expires_at" => expiresAt, "expires_in" => expiresIn,
"action" => action.rawValue,
] + "context" => context.contextStrings + keywordsParams))
}
public static func create(
title: String,
context: [FilterV1.Context],
expiresIn: TimeInterval?,
action: Action,
keywords keywordUpdates: [KeywordUpdate]
) -> Request<FilterV2> {
var keywordsParams = [Parameter]()
for (index, update) in keywordUpdates.enumerated() {
switch update {
case .add(keyword: let keyword, wholeWord: let wholeWord):
keywordsParams.append("keywords_attributes[\(index)][keyword]" => keyword)
keywordsParams.append("keywords_attributes[\(index)][whole_word]" => wholeWord)
default:
fatalError("can only add keywords when creating filter")
}
}
return Request(method: .post, path: "/api/v2/filters", body: ParametersBody([
"title" => title,
"expires_in" => expiresIn,
"action" => action.rawValue, "action" => action.rawValue,
] + "context" => context.contextStrings + keywordsParams)) ] + "context" => context.contextStrings + keywordsParams))
} }

View File

@ -17,6 +17,15 @@ class EditedFilter: ObservableObject {
@Published var keywords: [Keyword] @Published var keywords: [Keyword]
@Published var action: FilterV2.Action @Published var action: FilterV2.Action
init() {
self.id = nil
self.title = nil
self.contexts = [.home]
self.expiresIn = nil
self.keywords = [.init(id: nil, keyword: "", wholeWord: true)]
self.action = .warn
}
init(_ mo: FilterMO) { init(_ mo: FilterMO) {
self.id = mo.id self.id = mo.id
self.title = mo.title self.title = mo.title
@ -30,6 +39,28 @@ class EditedFilter: ObservableObject {
self.action = mo.filterAction self.action = mo.filterAction
} }
init(copying other: EditedFilter) {
self.id = other.id
self.title = other.title
self.contexts = other.contexts
self.expiresIn = other.expiresIn
self.keywords = other.keywords
self.action = other.action
}
func isValid(for mastodonController: MastodonController) -> Bool {
if mastodonController.instanceFeatures.filtersV2 && (title == nil || title!.isEmpty) {
return false
}
if keywords.isEmpty || keywords.contains(where: { $0.keyword.isEmpty }) {
return false
}
if contexts.isEmpty {
return false
}
return true
}
struct Keyword { struct Keyword {
let id: String? let id: String?
var keyword: String var keyword: String

View File

@ -28,21 +28,28 @@ struct EditFilterView: View {
}() }()
@ObservedObject var filter: EditedFilter @ObservedObject var filter: EditedFilter
let updateFilter: (EditedFilter) -> Void let create: Bool
let saveFilter: (EditedFilter) async throws -> Void
@EnvironmentObject private var mastodonController: MastodonController @EnvironmentObject private var mastodonController: MastodonController
@Environment(\.dismiss) private var dismiss
@State private var originalFilter: EditedFilter
@State private var edited = false @State private var edited = false
@State private var isSaving = false
@State private var saveError: (any Error)?
init(filter: EditedFilter, updateFilter: @escaping (EditedFilter) -> Void) { init(filter: EditedFilter, create: Bool, saveFilter: @escaping (EditedFilter) async throws -> Void) {
self.filter = filter self.filter = filter
self.updateFilter = updateFilter self.create = create
self.saveFilter = saveFilter
self._originalFilter = State(wrappedValue: EditedFilter(copying: filter))
if let expiresIn = filter.expiresIn { if let expiresIn = filter.expiresIn {
self.expiresIn = Self.expiresInOptions.min(by: { a, b in self._expiresIn = State(wrappedValue: Self.expiresInOptions.min(by: { a, b in
let aDist = abs(a.value - expiresIn) let aDist = abs(a.value - expiresIn)
let bDist = abs(b.value - expiresIn) let bDist = abs(b.value - expiresIn)
return aDist < bDist return aDist < bDist
})!.value })!.value)
} else { } else {
self.expiresIn = 24 * 60 * 60 self._expiresIn = State(wrappedValue: 24 * 60 * 60)
} }
} }
@ -144,14 +151,36 @@ struct EditFilterView: View {
} }
.navigationTitle("Edit Filter") .navigationTitle("Edit Filter")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
if isSaving {
ProgressView()
.progressViewStyle(.circular)
} else {
Button(create ? "Create" : "Save") {
Task {
do {
isSaving = true
try await saveFilter(filter)
dismiss()
} catch {
isSaving = false
saveError = error
}
}
}
.disabled(!filter.isValid(for: mastodonController) || !edited)
}
}
}
.alertWithData("Error Saving Filter", data: $saveError, actions: { _ in
Button("OK") {}
}, message: { error in
Text(error.localizedDescription)
})
.onReceive(filter.objectWillChange, perform: { _ in .onReceive(filter.objectWillChange, perform: { _ in
edited = true edited = true
}) })
.onDisappear {
if edited {
updateFilter(filter)
}
}
} }
} }

View File

@ -33,6 +33,10 @@ struct FilterRow: View {
} }
} }
if mastodonController.instanceFeatures.filtersV2 {
Text("^[\(filter.keywords.count) keywords](inflect: true)")
}
// 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(FilterV1.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)

View File

@ -50,6 +50,15 @@ struct FiltersList: View {
private var navigationBody: some View { private var navigationBody: some View {
List { List {
Section {
NavigationLink {
EditFilterView(filter: EditedFilter(), create: true, saveFilter: createFilter)
} label: {
Label("Add Filter", systemImage: "plus")
.foregroundColor(.accentColor)
}
}
filtersSection(unexpiredFilters) filtersSection(unexpiredFilters)
filtersSection(expiredFilters) filtersSection(expiredFilters)
} }
@ -85,7 +94,7 @@ struct FiltersList: View {
Section { Section {
ForEach(filters, id: \.id) { filter in ForEach(filters, id: \.id) { filter in
NavigationLink { NavigationLink {
EditFilterView(filter: EditedFilter(filter), updateFilter: updateFilter) EditFilterView(filter: EditedFilter(filter), create: false, saveFilter: updateFilter)
} label: { } label: {
FilterRow(filter: filter) FilterRow(filter: filter)
} }
@ -120,17 +129,11 @@ struct FiltersList: View {
} }
} }
private func updateFilter(_ filter: EditedFilter) { private func updateFilter(_ filter: EditedFilter) async throws {
Task { @MainActor in
do {
let mo = filters.first(where: { $0.id == filter.id })! let mo = filters.first(where: { $0.id == filter.id })!
let updateFrom: AnyFilter let updateFrom: AnyFilter
if mastodonController.instanceFeatures.filtersV2 { if mastodonController.instanceFeatures.filtersV2 {
var expiresAt: Date?
if let expiresIn = filter.expiresIn {
expiresAt = Date(timeIntervalSinceNow: expiresIn)
}
var updates = filter.keywords.map { var updates = filter.keywords.map {
if let id = $0.id { if let id = $0.id {
return FilterV2.KeywordUpdate.update(id: id, keyword: $0.keyword, wholeWord: $0.wholeWord) return FilterV2.KeywordUpdate.update(id: id, keyword: $0.keyword, wholeWord: $0.wholeWord)
@ -143,7 +146,7 @@ struct FiltersList: View {
updates.append(.destroy(id: 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 req = FilterV2.update(filter.id!, title: filter.title ?? "", context: filter.contexts, expiresIn: filter.expiresIn, action: filter.action, keywords: updates)
let (updated, _) = try await mastodonController.run(req) let (updated, _) = try await mastodonController.run(req)
updateFrom = .v2(updated) updateFrom = .v2(updated)
} else { } else {
@ -155,10 +158,26 @@ struct FiltersList: View {
let context = mastodonController.persistentContainer.viewContext let context = mastodonController.persistentContainer.viewContext
mo.updateFrom(apiFilter: updateFrom, context: context) mo.updateFrom(apiFilter: updateFrom, context: context)
mastodonController.persistentContainer.save(context: context) mastodonController.persistentContainer.save(context: context)
} catch {
self.updatingError = error
} }
private func createFilter(_ filter: EditedFilter) async throws {
let updateFrom: AnyFilter
if mastodonController.instanceFeatures.filtersV2 {
let updates = filter.keywords.map {
FilterV2.KeywordUpdate.add(keyword: $0.keyword, wholeWord: $0.wholeWord)
} }
let req = FilterV2.create(title: filter.title!, context: filter.contexts, expiresIn: filter.expiresIn, action: filter.action, keywords: updates)
let (updated, _) = try await mastodonController.run(req)
updateFrom = .v2(updated)
} else {
let req = Client.createFilterV1(phrase: filter.keywords.first!.keyword, context: filter.contexts, irreversible: nil, wholeWord: filter.keywords.first!.wholeWord, expiresIn: filter.expiresIn)
let (updated, _) = try await mastodonController.run(req)
updateFrom = .v1(updated)
}
let context = mastodonController.persistentContainer.viewContext
let mo = FilterMO(context: context)
mo.updateFrom(apiFilter: updateFrom, context: context)
mastodonController.persistentContainer.save(context: context)
} }
} }