Extract filter create/update/delete logic into separate services

This commit is contained in:
Shadowfacts 2022-12-03 14:25:06 -05:00
parent 83ca7f1321
commit f71804f094
7 changed files with 168 additions and 83 deletions

View File

@ -61,6 +61,9 @@
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 */; };
D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B0293BD85300C0B37F /* CreateFilterService.swift */; };
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B2293BD89C00C0B37F /* UpdateFilterService.swift */; };
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B4293BD97400C0B37F /* DeleteFilterService.swift */; };
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
@ -438,6 +441,9 @@
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>"; };
D61F75B0293BD85300C0B37F /* CreateFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFilterService.swift; sourceTree = "<group>"; };
D61F75B2293BD89C00C0B37F /* UpdateFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateFilterService.swift; sourceTree = "<group>"; };
D61F75B4293BD97400C0B37F /* DeleteFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteFilterService.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>"; };
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
@ -1567,6 +1573,9 @@
D6F6A551291F098700F496A8 /* RenameListService.swift */,
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */,
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */,
D61F75B0293BD85300C0B37F /* CreateFilterService.swift */,
D61F75B2293BD89C00C0B37F /* UpdateFilterService.swift */,
D61F75B4293BD97400C0B37F /* DeleteFilterService.swift */,
);
path = API;
sourceTree = "<group>";
@ -1949,6 +1958,7 @@
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
D6ADB6EE28EA74E8009924AB /* UIView+Configure.swift in Sources */,
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */,
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */,
@ -2055,6 +2065,7 @@
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */,
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */,
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */,
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
@ -2101,6 +2112,7 @@
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */,
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */,
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */,
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */,

View File

@ -0,0 +1,41 @@
//
// CreateFilterService.swift
// Tusker
//
// Created by Shadowfacts on 12/3/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
import Pachyderm
@MainActor
class CreateFilterService {
private let filter: EditedFilter
private let mastodonController: MastodonController
init(filter: EditedFilter, mastodonController: MastodonController) {
self.filter = filter
self.mastodonController = mastodonController
}
func run() 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)
}
}

View File

@ -0,0 +1,29 @@
//
// DeleteFilterService.swift
// Tusker
//
// Created by Shadowfacts on 12/3/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
import Pachyderm
@MainActor
class DeleteFilterService {
private let filter: FilterMO
private let mastodonController: MastodonController
init(filter: FilterMO, mastodonController: MastodonController) {
self.filter = filter
self.mastodonController = mastodonController
}
func run() async throws {
let req = FilterV1.delete(filter.id)
_ = try await mastodonController.run(req)
let context = mastodonController.persistentContainer.viewContext
context.delete(filter)
mastodonController.persistentContainer.save(context: context)
}
}

View File

@ -0,0 +1,52 @@
//
// UpdateFilterService.swift
// Tusker
//
// Created by Shadowfacts on 12/3/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
import Pachyderm
@MainActor
class UpdateFilterService {
private let filter: EditedFilter
private let mastodonController: MastodonController
init(filter: EditedFilter, mastodonController: MastodonController) {
self.filter = filter
self.mastodonController = mastodonController
}
func run() async throws {
let context = mastodonController.persistentContainer.viewContext
let mo = try context.fetch(FilterMO.fetchRequest(id: filter.id!)).first!
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 {
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, 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)
}
mo.updateFrom(apiFilter: updateFrom, context: context)
mastodonController.persistentContainer.save(context: context)
}
}

View File

@ -16,6 +16,12 @@ public final class FilterMO: NSManagedObject {
return NSFetchRequest(entityName: "Filter")
}
@nonobjc public class func fetchRequest(id: String) -> NSFetchRequest<FilterMO> {
let req = NSFetchRequest<FilterMO>(entityName: "Filter")
req.predicate = NSPredicate(format: "id = %@", id)
return req
}
@NSManaged public var id: String
@NSManaged public var title: String?
@NSManaged private var context: String

View File

@ -29,7 +29,6 @@ struct EditFilterView: View {
@ObservedObject var filter: EditedFilter
let create: Bool
let saveFilter: (EditedFilter) async throws -> Void
@EnvironmentObject private var mastodonController: MastodonController
@Environment(\.dismiss) private var dismiss
@State private var originalFilter: EditedFilter
@ -37,10 +36,9 @@ struct EditFilterView: View {
@State private var isSaving = false
@State private var saveError: (any Error)?
init(filter: EditedFilter, create: Bool, saveFilter: @escaping (EditedFilter) async throws -> Void) {
init(filter: EditedFilter, create: Bool) {
self.filter = filter
self.create = create
self.saveFilter = saveFilter
self._originalFilter = State(wrappedValue: EditedFilter(copying: filter))
if let expiresIn = filter.expiresIn {
self._expiresIn = State(wrappedValue: Self.expiresInOptions.min(by: { a, b in
@ -158,16 +156,7 @@ struct EditFilterView: View {
.progressViewStyle(.circular)
} else {
Button(create ? "Create" : "Save") {
Task {
do {
isSaving = true
try await saveFilter(filter)
dismiss()
} catch {
isSaving = false
saveError = error
}
}
saveFilter()
}
.disabled(!filter.isValid(for: mastodonController) || !edited)
}
@ -182,6 +171,23 @@ struct EditFilterView: View {
edited = true
})
}
private func saveFilter() {
Task {
do {
isSaving = true
if create {
try await CreateFilterService(filter: filter, mastodonController: mastodonController).run()
} else {
try await UpdateFilterService(filter: filter, mastodonController: mastodonController).run()
}
dismiss()
} catch {
isSaving = false
saveError = error
}
}
}
}
private struct FilterContextToggleStyle: ToggleStyle {

View File

@ -25,7 +25,6 @@ struct FiltersList: View {
@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, *) {
@ -52,15 +51,15 @@ struct FiltersList: View {
List {
Section {
NavigationLink {
EditFilterView(filter: EditedFilter(), create: true, saveFilter: createFilter)
EditFilterView(filter: EditedFilter(), create: true)
} label: {
Label("Add Filter", systemImage: "plus")
.foregroundColor(.accentColor)
}
}
filtersSection(unexpiredFilters)
filtersSection(expiredFilters)
filtersSection(unexpiredFilters, header: Text("Active"))
filtersSection(expiredFilters, header: Text("Expired"))
}
.navigationTitle(Text("Filters"))
.navigationBarTitleDisplayMode(.inline)
@ -78,23 +77,16 @@ struct FiltersList: View {
}, 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 {
private func filtersSection(_ filters: [FilterMO], header: some View) -> some View {
Section {
ForEach(filters, id: \.id) { filter in
NavigationLink {
EditFilterView(filter: EditedFilter(filter), create: false, saveFilter: updateFilter)
EditFilterView(filter: EditedFilter(filter), create: false)
} label: {
FilterRow(filter: filter)
}
@ -112,73 +104,20 @@ struct FiltersList: View {
deleteFilter(filter)
}
}
} header: {
header
}
}
private func deleteFilter(_ filter: FilterMO) {
Task { @MainActor in
let req = FilterV1.delete(filter.id)
Task {
do {
_ = try await mastodonController.run(req)
let context = mastodonController.persistentContainer.viewContext
context.delete(filter)
mastodonController.persistentContainer.save(context: context)
try await DeleteFilterService(filter: filter, mastodonController: mastodonController).run()
} catch {
self.deletionError = error
}
}
}
private func updateFilter(_ filter: EditedFilter) async throws {
let mo = filters.first(where: { $0.id == filter.id })!
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 {
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, 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)
}
}
//struct FiltersView_Previews: PreviewProvider {