From f71804f094b4596f836b8e3d7336ecf5b73836be Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 3 Dec 2022 14:25:06 -0500 Subject: [PATCH] Extract filter create/update/delete logic into separate services --- Tusker.xcodeproj/project.pbxproj | 12 ++++ Tusker/API/CreateFilterService.swift | 41 +++++++++++ Tusker/API/DeleteFilterService.swift | 29 ++++++++ Tusker/API/UpdateFilterService.swift | 52 ++++++++++++++ Tusker/CoreData/FilterMO.swift | 6 ++ Tusker/Screens/Filters/EditFilterView.swift | 32 +++++---- Tusker/Screens/Filters/FiltersView.swift | 79 +++------------------ 7 files changed, 168 insertions(+), 83 deletions(-) create mode 100644 Tusker/API/CreateFilterService.swift create mode 100644 Tusker/API/DeleteFilterService.swift create mode 100644 Tusker/API/UpdateFilterService.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index fcb02e5334..ee05863df3 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -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 = ""; }; D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Filter+Helpers.swift"; sourceTree = ""; }; D61F75AE293AF50C00C0B37F /* EditedFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedFilter.swift; sourceTree = ""; }; + D61F75B0293BD85300C0B37F /* CreateFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFilterService.swift; sourceTree = ""; }; + D61F75B2293BD89C00C0B37F /* UpdateFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateFilterService.swift; sourceTree = ""; }; + D61F75B4293BD97400C0B37F /* DeleteFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteFilterService.swift; sourceTree = ""; }; D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = ""; }; D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = ""; }; D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/Tusker/API/CreateFilterService.swift b/Tusker/API/CreateFilterService.swift new file mode 100644 index 0000000000..f1fb54c75a --- /dev/null +++ b/Tusker/API/CreateFilterService.swift @@ -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) + } +} diff --git a/Tusker/API/DeleteFilterService.swift b/Tusker/API/DeleteFilterService.swift new file mode 100644 index 0000000000..263c891a2f --- /dev/null +++ b/Tusker/API/DeleteFilterService.swift @@ -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) + } +} diff --git a/Tusker/API/UpdateFilterService.swift b/Tusker/API/UpdateFilterService.swift new file mode 100644 index 0000000000..8276a190f7 --- /dev/null +++ b/Tusker/API/UpdateFilterService.swift @@ -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) + } +} diff --git a/Tusker/CoreData/FilterMO.swift b/Tusker/CoreData/FilterMO.swift index fa16eacc22..da14c14d72 100644 --- a/Tusker/CoreData/FilterMO.swift +++ b/Tusker/CoreData/FilterMO.swift @@ -16,6 +16,12 @@ public final class FilterMO: NSManagedObject { return NSFetchRequest(entityName: "Filter") } + @nonobjc public class func fetchRequest(id: String) -> NSFetchRequest { + let req = NSFetchRequest(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 diff --git a/Tusker/Screens/Filters/EditFilterView.swift b/Tusker/Screens/Filters/EditFilterView.swift index 9e96e84a70..6833580b78 100644 --- a/Tusker/Screens/Filters/EditFilterView.swift +++ b/Tusker/Screens/Filters/EditFilterView.swift @@ -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 { diff --git a/Tusker/Screens/Filters/FiltersView.swift b/Tusker/Screens/Filters/FiltersView.swift index 2f61b46c44..248271c9a5 100644 --- a/Tusker/Screens/Filters/FiltersView.swift +++ b/Tusker/Screens/Filters/FiltersView.swift @@ -25,7 +25,6 @@ struct FiltersList: View { @FetchRequest(sortDescriptors: []) private var filters: FetchedResults @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 {