mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 08:10:59 +00:00
Filters WIP
This commit is contained in:
parent
ef0b0efd17
commit
b073f31e77
16 changed files with 570 additions and 12 deletions
|
@ -45,7 +45,7 @@ extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLists(_ lists: [MastodonList]) -> AnyPublisher<Never, Error> {
|
func setLists(_ lists: [MastodonList]) -> AnyPublisher<Never, Error> {
|
||||||
databaseQueue.writePublisher {
|
databaseQueue.writePublisher {
|
||||||
for list in lists {
|
for list in lists {
|
||||||
try Timeline.list(list).save($0)
|
try Timeline.list(list).save($0)
|
||||||
|
@ -65,10 +65,34 @@ extension ContentDatabase {
|
||||||
|
|
||||||
func deleteList(id: String) -> AnyPublisher<Never, Error> {
|
func deleteList(id: String) -> AnyPublisher<Never, Error> {
|
||||||
databaseQueue.writePublisher(updates: Timeline.filter(Column("id") == id).deleteAll)
|
databaseQueue.writePublisher(updates: Timeline.filter(Column("id") == id).deleteAll)
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFilters(_ filters: [Filter]) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseQueue.writePublisher {
|
||||||
|
for filter in filters {
|
||||||
|
try filter.save($0)
|
||||||
|
}
|
||||||
|
|
||||||
|
try Filter.filter(!filters.map(\.id).contains(Column("id"))).deleteAll($0)
|
||||||
|
}
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createFilter(_ filter: Filter) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseQueue.writePublisher(updates: filter.save)
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteFilter(id: String) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseQueue.writePublisher(updates: Filter.filter(Column("id") == id).deleteAll)
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func statusesObservation(timeline: Timeline) -> AnyPublisher<[Status], Error> {
|
func statusesObservation(timeline: Timeline) -> AnyPublisher<[Status], Error> {
|
||||||
ValueObservation
|
ValueObservation
|
||||||
.tracking(timeline.statuses
|
.tracking(timeline.statuses
|
||||||
|
@ -107,9 +131,16 @@ extension ContentDatabase {
|
||||||
ValueObservation.tracking(Timeline.filter(!Timeline.nonLists.map(\.id).contains(Column("id")))
|
ValueObservation.tracking(Timeline.filter(!Timeline.nonLists.map(\.id).contains(Column("id")))
|
||||||
.order(Column("listTitle").collating(.localizedCaseInsensitiveCompare).asc)
|
.order(Column("listTitle").collating(.localizedCaseInsensitiveCompare).asc)
|
||||||
.fetchAll)
|
.fetchAll)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseQueue)
|
.publisher(in: databaseQueue)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func filtersObservation() -> AnyPublisher<[Filter], Error> {
|
||||||
|
ValueObservation.tracking(Filter.fetchAll)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseQueue)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,6 +224,15 @@ private extension ContentDatabase {
|
||||||
|
|
||||||
t.primaryKey(["timelineId", "statusId"], onConflict: .replace)
|
t.primaryKey(["timelineId", "statusId"], onConflict: .replace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try db.create(table: "filter", ifNotExists: true) { t in
|
||||||
|
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
||||||
|
t.column("phrase", .text).notNull()
|
||||||
|
t.column("context", .blob).notNull()
|
||||||
|
t.column("expiresAt", .date)
|
||||||
|
t.column("irreversible", .boolean).notNull()
|
||||||
|
t.column("wholeWord", .boolean).notNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try migrator.migrate(writer)
|
try migrator.migrate(writer)
|
||||||
|
@ -297,6 +337,16 @@ private extension Timeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Filter: TableRecord, FetchableRecord, PersistableRecord {
|
||||||
|
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
||||||
|
MastodonDecoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
||||||
|
MastodonEncoder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct TransientStatusCollectionElement: Codable, TableRecord, FetchableRecord, PersistableRecord {
|
private struct TransientStatusCollectionElement: Codable, TableRecord, FetchableRecord, PersistableRecord {
|
||||||
let transientStatusCollectionId: String
|
let transientStatusCollectionId: String
|
||||||
let statusId: String
|
let statusId: String
|
||||||
|
|
|
@ -114,6 +114,14 @@ extension NotificationTypesPreferencesViewModel {
|
||||||
static let development = NotificationTypesPreferencesViewModel(identityService: .development)
|
static let development = NotificationTypesPreferencesViewModel(identityService: .development)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension FiltersViewModel {
|
||||||
|
static let development = FiltersViewModel(identityService: .development)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EditFilterViewModel {
|
||||||
|
static let development = EditFilterViewModel(filter: .new, identityService: .development)
|
||||||
|
}
|
||||||
|
|
||||||
extension StatusListViewModel {
|
extension StatusListViewModel {
|
||||||
static let development = StatusListViewModel(
|
static let development = StatusListViewModel(
|
||||||
statusListService: IdentityService.development.service(timeline: .home))
|
statusListService: IdentityService.development.service(timeline: .home))
|
||||||
|
|
|
@ -22,12 +22,30 @@
|
||||||
"preferences.expand-media.show-all" = "Show all";
|
"preferences.expand-media.show-all" = "Show all";
|
||||||
"preferences.expand-media.hide-all" = "Hide all";
|
"preferences.expand-media.hide-all" = "Hide all";
|
||||||
"preferences.reading-expand-spoilers" = "Always expand content warnings";
|
"preferences.reading-expand-spoilers" = "Always expand content warnings";
|
||||||
|
"preferences.filters" = "Filters";
|
||||||
"preferences.notification-types" = "Notification Types";
|
"preferences.notification-types" = "Notification Types";
|
||||||
"preferences.notification-types.follow" = "Follow";
|
"preferences.notification-types.follow" = "Follow";
|
||||||
"preferences.notification-types.favourite" = "Favorite";
|
"preferences.notification-types.favourite" = "Favorite";
|
||||||
"preferences.notification-types.reblog" = "Reblog";
|
"preferences.notification-types.reblog" = "Reblog";
|
||||||
"preferences.notification-types.mention" = "Mention";
|
"preferences.notification-types.mention" = "Mention";
|
||||||
"preferences.notification-types.poll" = "Poll";
|
"preferences.notification-types.poll" = "Poll";
|
||||||
|
"filter.add-new" = "Add New Filter";
|
||||||
|
"filter.edit" = "Edit Filter";
|
||||||
|
"filter.keyword-or-phrase" = "Keyword or phrase";
|
||||||
|
"filter.never-expires" = "Never expires";
|
||||||
|
"filter.expire-after" = "Expire after";
|
||||||
|
"filter.contexts" = "Filter contexts";
|
||||||
|
"filter.irreversible" = "Drop instead of hide";
|
||||||
|
"filter.irreversible-explanation" = "Filtered posts will disappear irreversibly, even if filter is later removed";
|
||||||
|
"filter.whole-word" = "Whole word";
|
||||||
|
"filter.whole-word-explanation" = "When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word";
|
||||||
|
"filter.save-changes" = "Save Changes";
|
||||||
|
"filter.context.home" = "Home timeline";
|
||||||
|
"filter.context.notifications" = "Notifications";
|
||||||
|
"filter.context.public" = "Public timelines";
|
||||||
|
"filter.context.thread" = "Conversations";
|
||||||
|
"filter.context.account" = "Profiles";
|
||||||
|
"filter.context.unknown" = "Unknown context";
|
||||||
"status.reblogged-by" = "%@ boosted";
|
"status.reblogged-by" = "%@ boosted";
|
||||||
"status.pinned-post" = "Pinned post";
|
"status.pinned-post" = "Pinned post";
|
||||||
"status.show-more" = "Show More";
|
"status.show-more" = "Show More";
|
||||||
|
|
|
@ -33,6 +33,13 @@
|
||||||
D0BEB1FD24F9E4E5001B0F04 /* ListsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */; };
|
D0BEB1FD24F9E4E5001B0F04 /* ListsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */; };
|
||||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */; };
|
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */; };
|
||||||
D0BEB20124FA0220001B0F04 /* ListEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20024FA0220001B0F04 /* ListEndpoint.swift */; };
|
D0BEB20124FA0220001B0F04 /* ListEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20024FA0220001B0F04 /* ListEndpoint.swift */; };
|
||||||
|
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20424FA1107001B0F04 /* FiltersView.swift */; };
|
||||||
|
D0BEB20724FA1121001B0F04 /* FiltersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20624FA1121001B0F04 /* FiltersViewModel.swift */; };
|
||||||
|
D0BEB20924FA1136001B0F04 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20824FA1136001B0F04 /* Filter.swift */; };
|
||||||
|
D0BEB20B24FA12D8001B0F04 /* FiltersEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20A24FA12D8001B0F04 /* FiltersEndpoint.swift */; };
|
||||||
|
D0BEB20D24FA193A001B0F04 /* FilterEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20C24FA193A001B0F04 /* FilterEndpoint.swift */; };
|
||||||
|
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */; };
|
||||||
|
D0BEB21324FA2C0A001B0F04 /* EditFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB21224FA2C0A001B0F04 /* EditFilterViewModel.swift */; };
|
||||||
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42224F76169001EBDBB /* IdentitiesView.swift */; };
|
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42224F76169001EBDBB /* IdentitiesView.swift */; };
|
||||||
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */; };
|
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */; };
|
||||||
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42424F76169001EBDBB /* AddIdentityView.swift */; };
|
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42424F76169001EBDBB /* AddIdentityView.swift */; };
|
||||||
|
@ -206,6 +213,13 @@
|
||||||
D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsViewModel.swift; sourceTree = "<group>"; };
|
D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsViewModel.swift; sourceTree = "<group>"; };
|
||||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsView.swift; sourceTree = "<group>"; };
|
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsView.swift; sourceTree = "<group>"; };
|
||||||
D0BEB20024FA0220001B0F04 /* ListEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListEndpoint.swift; sourceTree = "<group>"; };
|
D0BEB20024FA0220001B0F04 /* ListEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListEndpoint.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEB20424FA1107001B0F04 /* FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersView.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEB20624FA1121001B0F04 /* FiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEB20824FA1136001B0F04 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEB20A24FA12D8001B0F04 /* FiltersEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersEndpoint.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEB20C24FA193A001B0F04 /* FilterEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterEndpoint.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = "<group>"; };
|
||||||
|
D0BEB21224FA2C0A001B0F04 /* EditFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterViewModel.swift; sourceTree = "<group>"; };
|
||||||
D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; };
|
D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; };
|
||||||
D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentitiesView.swift; sourceTree = "<group>"; };
|
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentitiesView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -431,6 +445,8 @@
|
||||||
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
||||||
D01F41E024F8885900D55A2D /* Attachments */,
|
D01F41E024F8885900D55A2D /* Attachments */,
|
||||||
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
||||||
|
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||||
|
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
||||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
||||||
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
||||||
|
@ -474,6 +490,7 @@
|
||||||
D0C7D43A24F76169001EBDBB /* Attachment.swift */,
|
D0C7D43A24F76169001EBDBB /* Attachment.swift */,
|
||||||
D0C7D44124F76169001EBDBB /* Card.swift */,
|
D0C7D44124F76169001EBDBB /* Card.swift */,
|
||||||
D0C7D44B24F76169001EBDBB /* Emoji.swift */,
|
D0C7D44B24F76169001EBDBB /* Emoji.swift */,
|
||||||
|
D0BEB20824FA1136001B0F04 /* Filter.swift */,
|
||||||
D0C7D44224F76169001EBDBB /* HTML.swift */,
|
D0C7D44224F76169001EBDBB /* HTML.swift */,
|
||||||
D0C7D43B24F76169001EBDBB /* Identity.swift */,
|
D0C7D43B24F76169001EBDBB /* Identity.swift */,
|
||||||
D0C7D44524F76169001EBDBB /* Instance.swift */,
|
D0C7D44524F76169001EBDBB /* Instance.swift */,
|
||||||
|
@ -517,6 +534,8 @@
|
||||||
children = (
|
children = (
|
||||||
D0C7D46024F76169001EBDBB /* AddIdentityViewModel.swift */,
|
D0C7D46024F76169001EBDBB /* AddIdentityViewModel.swift */,
|
||||||
D01F41DE24F8868800D55A2D /* AttachmentViewModel.swift */,
|
D01F41DE24F8868800D55A2D /* AttachmentViewModel.swift */,
|
||||||
|
D0BEB21224FA2C0A001B0F04 /* EditFilterViewModel.swift */,
|
||||||
|
D0BEB20624FA1121001B0F04 /* FiltersViewModel.swift */,
|
||||||
D0C7D45F24F76169001EBDBB /* IdentitiesViewModel.swift */,
|
D0C7D45F24F76169001EBDBB /* IdentitiesViewModel.swift */,
|
||||||
D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */,
|
D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */,
|
||||||
D0C7D45D24F76169001EBDBB /* NotificationTypesPreferencesViewModel.swift */,
|
D0C7D45D24F76169001EBDBB /* NotificationTypesPreferencesViewModel.swift */,
|
||||||
|
@ -591,6 +610,8 @@
|
||||||
D0C7D47F24F76169001EBDBB /* AppAuthorizationEndpoint.swift */,
|
D0C7D47F24F76169001EBDBB /* AppAuthorizationEndpoint.swift */,
|
||||||
D0C7D48324F76169001EBDBB /* ContextEndpoint.swift */,
|
D0C7D48324F76169001EBDBB /* ContextEndpoint.swift */,
|
||||||
D0C7D48224F76169001EBDBB /* DeletionEndpoint.swift */,
|
D0C7D48224F76169001EBDBB /* DeletionEndpoint.swift */,
|
||||||
|
D0BEB20C24FA193A001B0F04 /* FilterEndpoint.swift */,
|
||||||
|
D0BEB20A24FA12D8001B0F04 /* FiltersEndpoint.swift */,
|
||||||
D0C7D47D24F76169001EBDBB /* InstanceEndpoint.swift */,
|
D0C7D47D24F76169001EBDBB /* InstanceEndpoint.swift */,
|
||||||
D0BEB20024FA0220001B0F04 /* ListEndpoint.swift */,
|
D0BEB20024FA0220001B0F04 /* ListEndpoint.swift */,
|
||||||
D0BEB1F824F9D627001B0F04 /* ListsEndpoint.swift */,
|
D0BEB1F824F9D627001B0F04 /* ListsEndpoint.swift */,
|
||||||
|
@ -874,14 +895,17 @@
|
||||||
D0C7D4D224F7616A001EBDBB /* ContentDatabase.swift in Sources */,
|
D0C7D4D224F7616A001EBDBB /* ContentDatabase.swift in Sources */,
|
||||||
D0C7D4F724F7616A001EBDBB /* StatusService.swift in Sources */,
|
D0C7D4F724F7616A001EBDBB /* StatusService.swift in Sources */,
|
||||||
D04FD73924D4A7B4007D572D /* AccountEndpoint+Stubbing.swift in Sources */,
|
D04FD73924D4A7B4007D572D /* AccountEndpoint+Stubbing.swift in Sources */,
|
||||||
|
D0BEB21324FA2C0A001B0F04 /* EditFilterViewModel.swift in Sources */,
|
||||||
D0C7D4FA24F7616A001EBDBB /* AllIdentitiesService.swift in Sources */,
|
D0C7D4FA24F7616A001EBDBB /* AllIdentitiesService.swift in Sources */,
|
||||||
D0C7D4CD24F7616A001EBDBB /* AddIdentityViewModel.swift in Sources */,
|
D0C7D4CD24F7616A001EBDBB /* AddIdentityViewModel.swift in Sources */,
|
||||||
D03658D124EDD80900AC17EC /* ContextEndpoint+Stubbing.swift in Sources */,
|
D03658D124EDD80900AC17EC /* ContextEndpoint+Stubbing.swift in Sources */,
|
||||||
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */,
|
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */,
|
||||||
D0BEB1F524F9A216001B0F04 /* Paged.swift in Sources */,
|
D0BEB1F524F9A216001B0F04 /* Paged.swift in Sources */,
|
||||||
D0DC174A24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */,
|
D0DC174A24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */,
|
||||||
|
D0BEB20D24FA193A001B0F04 /* FilterEndpoint.swift in Sources */,
|
||||||
D0C7D49A24F7616A001EBDBB /* StatusListView.swift in Sources */,
|
D0C7D49A24F7616A001EBDBB /* StatusListView.swift in Sources */,
|
||||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
||||||
|
D0BEB20924FA1136001B0F04 /* Filter.swift in Sources */,
|
||||||
D0C7D4A524F7616A001EBDBB /* StatusListViewController.swift in Sources */,
|
D0C7D4A524F7616A001EBDBB /* StatusListViewController.swift in Sources */,
|
||||||
D0C7D4CC24F7616A001EBDBB /* IdentitiesViewModel.swift in Sources */,
|
D0C7D4CC24F7616A001EBDBB /* IdentitiesViewModel.swift in Sources */,
|
||||||
D0C7D4E024F7616A001EBDBB /* WebAuthSession.swift in Sources */,
|
D0C7D4E024F7616A001EBDBB /* WebAuthSession.swift in Sources */,
|
||||||
|
@ -952,10 +976,13 @@
|
||||||
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */,
|
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */,
|
||||||
D0C7D4AA24F7616A001EBDBB /* Attachment.swift in Sources */,
|
D0C7D4AA24F7616A001EBDBB /* Attachment.swift in Sources */,
|
||||||
D0C7D4AF24F7616A001EBDBB /* PushNotification.swift in Sources */,
|
D0C7D4AF24F7616A001EBDBB /* PushNotification.swift in Sources */,
|
||||||
|
D0BEB20724FA1121001B0F04 /* FiltersViewModel.swift in Sources */,
|
||||||
D0C7D4C924F7616A001EBDBB /* TabNavigationViewModel.swift in Sources */,
|
D0C7D4C924F7616A001EBDBB /* TabNavigationViewModel.swift in Sources */,
|
||||||
|
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
|
||||||
D0C7D4B624F7616A001EBDBB /* ListTimeline.swift in Sources */,
|
D0C7D4B624F7616A001EBDBB /* ListTimeline.swift in Sources */,
|
||||||
D0C7D4E124F7616A001EBDBB /* MastodonDecoder.swift in Sources */,
|
D0C7D4E124F7616A001EBDBB /* MastodonDecoder.swift in Sources */,
|
||||||
D0C7D4B024F7616A001EBDBB /* PushSubscription.swift in Sources */,
|
D0C7D4B024F7616A001EBDBB /* PushSubscription.swift in Sources */,
|
||||||
|
D0BEB20B24FA12D8001B0F04 /* FiltersEndpoint.swift in Sources */,
|
||||||
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */,
|
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */,
|
||||||
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
||||||
D0C7D4E424F7616A001EBDBB /* PreferencesEndpoint.swift in Sources */,
|
D0C7D4E424F7616A001EBDBB /* PreferencesEndpoint.swift in Sources */,
|
||||||
|
@ -966,6 +993,7 @@
|
||||||
D0C7D4F324F7616A001EBDBB /* ContextService.swift in Sources */,
|
D0C7D4F324F7616A001EBDBB /* ContextService.swift in Sources */,
|
||||||
D0C7D4DD24F7616A001EBDBB /* CodingUserInfoKey+Extensions.swift in Sources */,
|
D0C7D4DD24F7616A001EBDBB /* CodingUserInfoKey+Extensions.swift in Sources */,
|
||||||
D0C7D4D824F7616A001EBDBB /* Publisher+Extensions.swift in Sources */,
|
D0C7D4D824F7616A001EBDBB /* Publisher+Extensions.swift in Sources */,
|
||||||
|
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
||||||
D01F41D824F880C400D55A2D /* StatusTableViewCell.swift in Sources */,
|
D01F41D824F880C400D55A2D /* StatusTableViewCell.swift in Sources */,
|
||||||
D04FD73C24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */,
|
D04FD73C24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */,
|
||||||
D0C7D4BE24F7616A001EBDBB /* Unknowable.swift in Sources */,
|
D0C7D4BE24F7616A001EBDBB /* Unknowable.swift in Sources */,
|
||||||
|
|
56
Model/Filter.swift
Normal file
56
Model/Filter.swift
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Filter: Codable, Hashable, Identifiable {
|
||||||
|
enum Context: String, Codable, Unknowable {
|
||||||
|
case home
|
||||||
|
case notifications
|
||||||
|
case `public`
|
||||||
|
case thread
|
||||||
|
case account
|
||||||
|
case unknown
|
||||||
|
|
||||||
|
static var unknownCase: Self { .unknown }
|
||||||
|
}
|
||||||
|
|
||||||
|
let id: String
|
||||||
|
var phrase: String
|
||||||
|
var context: [Context]
|
||||||
|
var expiresAt: Date?
|
||||||
|
var irreversible: Bool
|
||||||
|
var wholeWord: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Filter {
|
||||||
|
static let newFilterID: String = "com.metabolist.metatext.new-filter-id"
|
||||||
|
static let new = Self(id: newFilterID,
|
||||||
|
phrase: "",
|
||||||
|
context: [],
|
||||||
|
expiresAt: nil,
|
||||||
|
irreversible: false,
|
||||||
|
wholeWord: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Filter.Context: Identifiable {
|
||||||
|
var id: Self { self }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Filter.Context {
|
||||||
|
var localized: String {
|
||||||
|
switch self {
|
||||||
|
case .home:
|
||||||
|
return NSLocalizedString("filter.context.home", comment: "")
|
||||||
|
case .notifications:
|
||||||
|
return NSLocalizedString("filter.context.notifications", comment: "")
|
||||||
|
case .public:
|
||||||
|
return NSLocalizedString("filter.context.public", comment: "")
|
||||||
|
case .thread:
|
||||||
|
return NSLocalizedString("filter.context.thread", comment: "")
|
||||||
|
case .account:
|
||||||
|
return NSLocalizedString("filter.context.account", comment: "")
|
||||||
|
case .unknown:
|
||||||
|
return NSLocalizedString("filter.context.unknown", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import Foundation
|
||||||
enum DeletionEndpoint {
|
enum DeletionEndpoint {
|
||||||
case oauthRevoke(token: String, clientID: String, clientSecret: String)
|
case oauthRevoke(token: String, clientID: String, clientSecret: String)
|
||||||
case list(id: String)
|
case list(id: String)
|
||||||
|
case filter(id: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DeletionEndpoint: MastodonEndpoint {
|
extension DeletionEndpoint: MastodonEndpoint {
|
||||||
|
@ -16,6 +17,8 @@ extension DeletionEndpoint: MastodonEndpoint {
|
||||||
return ["oauth"]
|
return ["oauth"]
|
||||||
case .list:
|
case .list:
|
||||||
return defaultContext + ["lists"]
|
return defaultContext + ["lists"]
|
||||||
|
case .filter:
|
||||||
|
return defaultContext + ["filters"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +26,7 @@ extension DeletionEndpoint: MastodonEndpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case .oauthRevoke:
|
case .oauthRevoke:
|
||||||
return ["revoke"]
|
return ["revoke"]
|
||||||
case let .list(id):
|
case let .list(id), let .filter(id):
|
||||||
return [id]
|
return [id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +35,7 @@ extension DeletionEndpoint: MastodonEndpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case .oauthRevoke:
|
case .oauthRevoke:
|
||||||
return .post
|
return .post
|
||||||
case .list:
|
case .list, .filter:
|
||||||
return .delete
|
return .delete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +44,7 @@ extension DeletionEndpoint: MastodonEndpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case let .oauthRevoke(token, clientID, clientSecret):
|
case let .oauthRevoke(token, clientID, clientSecret):
|
||||||
return ["token": token, "client_id": clientID, "client_secret": clientSecret]
|
return ["token": token, "client_id": clientID, "client_secret": clientSecret]
|
||||||
case .list:
|
case .list, .filter:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
94
Networking/Mastodon API/Endpoints/FilterEndpoint.swift
Normal file
94
Networking/Mastodon API/Endpoints/FilterEndpoint.swift
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum FilterEndpoint {
|
||||||
|
case create(
|
||||||
|
phrase: String,
|
||||||
|
context: [Filter.Context],
|
||||||
|
irreversible: Bool,
|
||||||
|
wholeWord: Bool,
|
||||||
|
expiresIn: Date?)
|
||||||
|
case update(
|
||||||
|
id: String,
|
||||||
|
phrase: String,
|
||||||
|
context: [Filter.Context],
|
||||||
|
irreversible: Bool,
|
||||||
|
wholeWord: Bool,
|
||||||
|
expiresIn: Date?)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FilterEndpoint: MastodonEndpoint {
|
||||||
|
typealias ResultType = Filter
|
||||||
|
|
||||||
|
var context: [String] {
|
||||||
|
defaultContext + ["filters"]
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathComponentsInContext: [String] {
|
||||||
|
switch self {
|
||||||
|
case .create:
|
||||||
|
return []
|
||||||
|
case let .update(id, _, _, _, _, _):
|
||||||
|
return [id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters: [String: Any]? {
|
||||||
|
switch self {
|
||||||
|
case let .create(phrase, context, irreversible, wholeWord, expiresIn):
|
||||||
|
return params(phrase: phrase,
|
||||||
|
context: context,
|
||||||
|
irreversible: irreversible,
|
||||||
|
wholeWord: wholeWord,
|
||||||
|
expiresIn: expiresIn)
|
||||||
|
case let .update(id, phrase, context, irreversible, wholeWord, expiresIn):
|
||||||
|
var params = self.params(phrase: phrase,
|
||||||
|
context: context,
|
||||||
|
irreversible: irreversible,
|
||||||
|
wholeWord: wholeWord,
|
||||||
|
expiresIn: expiresIn)
|
||||||
|
|
||||||
|
params["id"] = id
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .create:
|
||||||
|
return .post
|
||||||
|
case .update:
|
||||||
|
return .put
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension FilterEndpoint {
|
||||||
|
func params(phrase: String,
|
||||||
|
context: [Filter.Context],
|
||||||
|
irreversible: Bool,
|
||||||
|
wholeWord: Bool,
|
||||||
|
expiresIn: Date?) -> [String: Any] {
|
||||||
|
var params: [String: Any] = [
|
||||||
|
"phrase": phrase,
|
||||||
|
"context": context.map(\.rawValue),
|
||||||
|
"irreversible": irreversible,
|
||||||
|
"whole_word": wholeWord]
|
||||||
|
|
||||||
|
if let expiresIn = expiresIn {
|
||||||
|
params["expires_in"] = Self.dateFormatter.string(from: expiresIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
static let dateFormatter: DateFormatter = {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
|
||||||
|
dateFormatter.dateFormat = MastodonAPI.dateFormat
|
||||||
|
|
||||||
|
return dateFormatter
|
||||||
|
}()
|
||||||
|
}
|
29
Networking/Mastodon API/Endpoints/FiltersEndpoint.swift
Normal file
29
Networking/Mastodon API/Endpoints/FiltersEndpoint.swift
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum FiltersEndpoint {
|
||||||
|
case filters
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FiltersEndpoint: MastodonEndpoint {
|
||||||
|
typealias ResultType = [Filter]
|
||||||
|
|
||||||
|
var context: [String] {
|
||||||
|
defaultContext + ["filters"]
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathComponentsInContext: [String] {
|
||||||
|
switch self {
|
||||||
|
case .filters:
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .filters:
|
||||||
|
return .get
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -88,14 +88,10 @@ extension IdentityService {
|
||||||
|
|
||||||
func refreshLists() -> AnyPublisher<Never, Error> {
|
func refreshLists() -> AnyPublisher<Never, Error> {
|
||||||
networkClient.request(ListsEndpoint.lists)
|
networkClient.request(ListsEndpoint.lists)
|
||||||
.flatMap(contentDatabase.updateLists(_:))
|
.flatMap(contentDatabase.setLists(_:))
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
|
||||||
contentDatabase.listsObservation()
|
|
||||||
}
|
|
||||||
|
|
||||||
func createList(title: String) -> AnyPublisher<Never, Error> {
|
func createList(title: String) -> AnyPublisher<Never, Error> {
|
||||||
networkClient.request(ListEndpoint.create(title: title))
|
networkClient.request(ListEndpoint.create(title: title))
|
||||||
.flatMap(contentDatabase.createList(_:))
|
.flatMap(contentDatabase.createList(_:))
|
||||||
|
@ -109,6 +105,48 @@ extension IdentityService {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
||||||
|
contentDatabase.listsObservation()
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshFilters() -> AnyPublisher<Never, Error> {
|
||||||
|
networkClient.request(FiltersEndpoint.filters)
|
||||||
|
.flatMap(contentDatabase.setFilters(_:))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFilter(_ filter: Filter) -> AnyPublisher<Never, Error> {
|
||||||
|
networkClient.request(FilterEndpoint.create(phrase: filter.phrase,
|
||||||
|
context: filter.context,
|
||||||
|
irreversible: filter.irreversible,
|
||||||
|
wholeWord: filter.wholeWord,
|
||||||
|
expiresIn: filter.expiresAt))
|
||||||
|
.flatMap(contentDatabase.createFilter(_:))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFilter(_ filter: Filter) -> AnyPublisher<Never, Error> {
|
||||||
|
networkClient.request(FilterEndpoint.update(id: filter.id,
|
||||||
|
phrase: filter.phrase,
|
||||||
|
context: filter.context,
|
||||||
|
irreversible: filter.irreversible,
|
||||||
|
wholeWord: filter.wholeWord,
|
||||||
|
expiresIn: filter.expiresAt))
|
||||||
|
.flatMap(contentDatabase.createFilter(_:))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteFilter(id: String) -> AnyPublisher<Never, Error> {
|
||||||
|
networkClient.request(DeletionEndpoint.filter(id: id))
|
||||||
|
.map { _ in id }
|
||||||
|
.flatMap(contentDatabase.deleteFilter(id:))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func filtersObservation() -> AnyPublisher<[Filter], Error> {
|
||||||
|
contentDatabase.filtersObservation()
|
||||||
|
}
|
||||||
|
|
||||||
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Never, Error> {
|
func updatePreferences(_ preferences: Identity.Preferences) -> AnyPublisher<Never, Error> {
|
||||||
identityDatabase.updatePreferences(preferences, forIdentityID: identity.id)
|
identityDatabase.updatePreferences(preferences, forIdentityID: identity.id)
|
||||||
.zip(Just(self).first().setFailureType(to: Error.self))
|
.zip(Just(self).first().setFailureType(to: Error.self))
|
||||||
|
|
57
View Models/EditFilterViewModel.swift
Normal file
57
View Models/EditFilterViewModel.swift
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class EditFilterViewModel: ObservableObject {
|
||||||
|
@Published var filter: Filter
|
||||||
|
@Published var saving = false
|
||||||
|
@Published var alertItem: AlertItem?
|
||||||
|
var date: Date
|
||||||
|
let dateRange: ClosedRange<Date>
|
||||||
|
let saveCompleted: AnyPublisher<Void, Never>
|
||||||
|
|
||||||
|
private let identityService: IdentityService
|
||||||
|
private let saveCompletedInput = PassthroughSubject<Void, Never>()
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(filter: Filter, identityService: IdentityService) {
|
||||||
|
self.filter = filter
|
||||||
|
self.identityService = identityService
|
||||||
|
date = Calendar.autoupdatingCurrent.date(byAdding: .minute, value: 30, to: Date()) ?? Date()
|
||||||
|
dateRange = date...(Calendar.autoupdatingCurrent.date(byAdding: .day, value: 7, to: date) ?? Date())
|
||||||
|
saveCompleted = saveCompletedInput.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EditFilterViewModel {
|
||||||
|
var isNew: Bool { filter.id == Filter.newFilterID }
|
||||||
|
|
||||||
|
var isSaveDisabled: Bool { filter.phrase == "" || filter.context.isEmpty }
|
||||||
|
|
||||||
|
func toggleSelection(context: Filter.Context) {
|
||||||
|
if filter.context.contains(context) {
|
||||||
|
filter.context.removeAll { $0 == context }
|
||||||
|
} else {
|
||||||
|
filter.context.append(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func save() {
|
||||||
|
(isNew ? identityService.createFilter(filter) : identityService.updateFilter(filter))
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.handleEvents(
|
||||||
|
receiveSubscription: { [weak self] _ in self?.saving = true },
|
||||||
|
receiveCompletion: { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.saving = false
|
||||||
|
|
||||||
|
if case .finished = $0 {
|
||||||
|
self.saveCompletedInput.send(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sink { _ in }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
33
View Models/FiltersViewModel.swift
Normal file
33
View Models/FiltersViewModel.swift
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class FiltersViewModel: ObservableObject {
|
||||||
|
@Published var filters = [Filter]()
|
||||||
|
@Published var alertItem: AlertItem?
|
||||||
|
|
||||||
|
private let identityService: IdentityService
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(identityService: IdentityService) {
|
||||||
|
self.identityService = identityService
|
||||||
|
|
||||||
|
identityService.filtersObservation()
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.assign(to: &$filters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FiltersViewModel {
|
||||||
|
func refreshFilters() {
|
||||||
|
identityService.refreshFilters()
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.sink { _ in }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func editFilterViewModel(filter: Filter) -> EditFilterViewModel {
|
||||||
|
EditFilterViewModel(filter: filter, identityService: identityService)
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,4 +24,8 @@ extension PreferencesViewModel {
|
||||||
func notificationTypesPreferencesViewModel() -> NotificationTypesPreferencesViewModel {
|
func notificationTypesPreferencesViewModel() -> NotificationTypesPreferencesViewModel {
|
||||||
NotificationTypesPreferencesViewModel(identityService: identityService)
|
NotificationTypesPreferencesViewModel(identityService: identityService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filtersViewModel() -> FiltersViewModel {
|
||||||
|
FiltersViewModel(identityService: identityService)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,11 @@ extension TabNavigationViewModel {
|
||||||
.sink { _ in }
|
.sink { _ in }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
identityService.refreshFilters()
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.sink { _ in }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
if identity.preferences.useServerPostingReadingPreferences {
|
if identity.preferences.useServerPostingReadingPreferences {
|
||||||
identityService.refreshServerPreferences()
|
identityService.refreshServerPreferences()
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
|
90
Views/EditFilterView.swift
Normal file
90
Views/EditFilterView.swift
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EditFilterView: View {
|
||||||
|
@StateObject var viewModel: EditFilterViewModel
|
||||||
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section(header: Text("filter.keyword-or-phrase")) {
|
||||||
|
TextField("filter.keyword-or-phrase", text: $viewModel.filter.phrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
if viewModel.isNew || viewModel.filter.expiresAt == nil {
|
||||||
|
Toggle("filter.never-expires", isOn: .init(
|
||||||
|
get: { viewModel.filter.expiresAt == nil },
|
||||||
|
set: {
|
||||||
|
if $0 {
|
||||||
|
viewModel.filter.expiresAt = nil
|
||||||
|
} else {
|
||||||
|
viewModel.filter.expiresAt = viewModel.date
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.filter.expiresAt != nil {
|
||||||
|
DatePicker(selection: $viewModel.date, in: viewModel.dateRange) {
|
||||||
|
Text("filter.expire-after")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("filter.contexts")) {
|
||||||
|
ForEach(Filter.Context.allCasesExceptUnknown) { context in
|
||||||
|
Toggle(context.localized, isOn: .init(
|
||||||
|
get: { viewModel.filter.context.contains(context) },
|
||||||
|
set: { _ in viewModel.toggleSelection(context: context) }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Toggle(isOn: $viewModel.filter.irreversible, label: {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("filter.irreversible")
|
||||||
|
Text("filter.irreversible-explanation")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Toggle(isOn: $viewModel.filter.wholeWord, label: {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("filter.whole-word")
|
||||||
|
Text("filter.whole-word-explanation")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alertItem($viewModel.alertItem)
|
||||||
|
.onReceive(viewModel.saveCompleted) { presentationMode.wrappedValue.dismiss() }
|
||||||
|
.navigationTitle(viewModel.isNew ? "filter.add-new" : "filter.edit")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Group {
|
||||||
|
if viewModel.saving {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
Button(viewModel.isNew ? "add" : "filter.save-changes",
|
||||||
|
action: viewModel.save)
|
||||||
|
.disabled(viewModel.isSaveDisabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
struct EditFilterView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
EditFilterView(viewModel: .development)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
42
Views/FiltersView.swift
Normal file
42
Views/FiltersView.swift
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FiltersView: View {
|
||||||
|
@StateObject var viewModel: FiltersViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
NavigationLink(destination: EditFilterView(
|
||||||
|
viewModel: viewModel.editFilterViewModel(filter: .new))) {
|
||||||
|
Label("add", systemImage: "plus.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
ForEach(viewModel.filters) { filter in
|
||||||
|
NavigationLink(destination: EditFilterView(
|
||||||
|
viewModel: viewModel.editFilterViewModel(filter: filter))) {
|
||||||
|
HStack {
|
||||||
|
Text(filter.phrase)
|
||||||
|
Spacer()
|
||||||
|
Text(ListFormatter.localizedString(byJoining: filter.context.map(\.localized)))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("preferences.filters")
|
||||||
|
.alertItem($viewModel.alertItem)
|
||||||
|
.onAppear(perform: viewModel.refreshFilters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
struct FiltersView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
FiltersView(viewModel: .development)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -11,6 +11,9 @@ struct PreferencesView: View {
|
||||||
NavigationLink("preferences.posting-reading",
|
NavigationLink("preferences.posting-reading",
|
||||||
destination: PostingReadingPreferencesView(
|
destination: PostingReadingPreferencesView(
|
||||||
viewModel: viewModel.postingReadingPreferencesViewModel()))
|
viewModel: viewModel.postingReadingPreferencesViewModel()))
|
||||||
|
NavigationLink("preferences.filters",
|
||||||
|
destination: FiltersView(
|
||||||
|
viewModel: viewModel.filtersViewModel()))
|
||||||
if viewModel.shouldShowNotificationTypePreferences {
|
if viewModel.shouldShowNotificationTypePreferences {
|
||||||
NavigationLink("preferences.notification-types",
|
NavigationLink("preferences.notification-types",
|
||||||
destination: NotificationTypesPreferencesView(
|
destination: NotificationTypesPreferencesView(
|
||||||
|
|
Loading…
Reference in a new issue