forked from shadowfacts/Tusker
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")
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,45 +129,55 @@ struct FiltersList: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateFilter(_ filter: EditedFilter) {
|
private func updateFilter(_ filter: EditedFilter) async throws {
|
||||||
Task { @MainActor in
|
let mo = filters.first(where: { $0.id == filter.id })!
|
||||||
do {
|
|
||||||
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?
|
var updates = filter.keywords.map {
|
||||||
if let expiresIn = filter.expiresIn {
|
if let id = $0.id {
|
||||||
expiresAt = Date(timeIntervalSinceNow: expiresIn)
|
return FilterV2.KeywordUpdate.update(id: id, keyword: $0.keyword, wholeWord: $0.wholeWord)
|
||||||
}
|
|
||||||
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 {
|
} 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)
|
return FilterV2.KeywordUpdate.add(keyword: $0.keyword, wholeWord: $0.wholeWord)
|
||||||
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 {
|
|
||||||
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