Creating filters UI
This commit is contained in:
parent
16a1e4008b
commit
83ca7f1321
|
@ -233,12 +233,12 @@ public class Client {
|
|||
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([
|
||||
"phrase" => phrase,
|
||||
"irreversible" => irreversible,
|
||||
"whole_word" => wholeWord,
|
||||
"expires_at" => expiresAt
|
||||
"expires_in" => expiresIn,
|
||||
] + "context" => context.contextStrings))
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ public struct FilterV2: Decodable {
|
|||
_ filterID: String,
|
||||
title: String,
|
||||
context: [FilterV1.Context],
|
||||
expiresAt: Date?,
|
||||
expiresIn: TimeInterval?,
|
||||
action: Action,
|
||||
keywords keywordUpdates: [KeywordUpdate]
|
||||
) -> Request<FilterV2> {
|
||||
|
@ -40,7 +40,31 @@ public struct FilterV2: Decodable {
|
|||
}
|
||||
return Request(method: .put, path: "/api/v2/filters/\(filterID)", body: ParametersBody([
|
||||
"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,
|
||||
] + "context" => context.contextStrings + keywordsParams))
|
||||
}
|
||||
|
|
|
@ -17,6 +17,15 @@ class EditedFilter: ObservableObject {
|
|||
@Published var keywords: [Keyword]
|
||||
@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) {
|
||||
self.id = mo.id
|
||||
self.title = mo.title
|
||||
|
@ -30,6 +39,28 @@ class EditedFilter: ObservableObject {
|
|||
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 {
|
||||
let id: String?
|
||||
var keyword: String
|
||||
|
|
|
@ -28,21 +28,28 @@ struct EditFilterView: View {
|
|||
}()
|
||||
|
||||
@ObservedObject var filter: EditedFilter
|
||||
let updateFilter: (EditedFilter) -> Void
|
||||
let create: Bool
|
||||
let saveFilter: (EditedFilter) async throws -> Void
|
||||
@EnvironmentObject private var mastodonController: MastodonController
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var originalFilter: EditedFilter
|
||||
@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.updateFilter = updateFilter
|
||||
self.create = create
|
||||
self.saveFilter = saveFilter
|
||||
self._originalFilter = State(wrappedValue: EditedFilter(copying: filter))
|
||||
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 bDist = abs(b.value - expiresIn)
|
||||
return aDist < bDist
|
||||
})!.value
|
||||
})!.value)
|
||||
} else {
|
||||
self.expiresIn = 24 * 60 * 60
|
||||
self._expiresIn = State(wrappedValue: 24 * 60 * 60)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,14 +151,36 @@ struct EditFilterView: View {
|
|||
}
|
||||
.navigationTitle("Edit Filter")
|
||||
.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
|
||||
edited = true
|
||||
})
|
||||
.onDisappear {
|
||||
if edited {
|
||||
updateFilter(filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
Text(FilterV1.Context.allCases.filter { filter.contexts.contains($0) }.map(\.displayName).formatted())
|
||||
.font(.subheadline)
|
||||
|
|
|
@ -50,6 +50,15 @@ struct FiltersList: View {
|
|||
|
||||
private var navigationBody: some View {
|
||||
List {
|
||||
Section {
|
||||
NavigationLink {
|
||||
EditFilterView(filter: EditedFilter(), create: true, saveFilter: createFilter)
|
||||
} label: {
|
||||
Label("Add Filter", systemImage: "plus")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
filtersSection(unexpiredFilters)
|
||||
filtersSection(expiredFilters)
|
||||
}
|
||||
|
@ -85,7 +94,7 @@ struct FiltersList: View {
|
|||
Section {
|
||||
ForEach(filters, id: \.id) { filter in
|
||||
NavigationLink {
|
||||
EditFilterView(filter: EditedFilter(filter), updateFilter: updateFilter)
|
||||
EditFilterView(filter: EditedFilter(filter), create: false, saveFilter: updateFilter)
|
||||
} label: {
|
||||
FilterRow(filter: filter)
|
||||
}
|
||||
|
@ -120,45 +129,55 @@ struct FiltersList: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func updateFilter(_ filter: EditedFilter) {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let mo = filters.first(where: { $0.id == filter.id })!
|
||||
private func updateFilter(_ filter: EditedFilter) async throws {
|
||||
let mo = filters.first(where: { $0.id == filter.id })!
|
||||
|
||||
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)
|
||||
let updateFrom: AnyFilter
|
||||
if mastodonController.instanceFeatures.filtersV2 {
|
||||
var updates = filter.keywords.map {
|
||||
if let id = $0.id {
|
||||
return FilterV2.KeywordUpdate.update(id: id, keyword: $0.keyword, wholeWord: $0.wholeWord)
|
||||
} 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)
|
||||
return FilterV2.KeywordUpdate.add(keyword: $0.keyword, wholeWord: $0.wholeWord)
|
||||
}
|
||||
|
||||
let context = mastodonController.persistentContainer.viewContext
|
||||
mo.updateFrom(apiFilter: updateFrom, context: context)
|
||||
mastodonController.persistentContainer.save(context: context)
|
||||
} catch {
|
||||
self.updatingError = error
|
||||
}
|
||||
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, expiresIn: filter.expiresIn, 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue