krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.9.0: Hutch/Views/Settings/NotificationPreferencesViewModel.swift · raw
1import Foundation
2
3// MARK: - Response types (file-private to avoid @MainActor Decodable issues)
4
5private struct TodoPreferencesResponse: Decodable, Sendable {
6 let preferences: TodoPreferences
7}
8
9private struct TodoPreferences: Decodable, Sendable {
10 let notifySelf: Bool
11}
12
13private struct ListsPreferencesResponse: Decodable, Sendable {
14 let preferences: ListsPreferences
15}
16
17private struct ListsPreferences: Decodable, Sendable {
18 let copySelf: Bool
19}
20
21// MARK: - View Model
22
23/// Email preferences for todo.sr.ht and lists.sr.ht.
24///
25/// The two services each expose `preferences`/`updatePreferences` under the same
26/// names but with different fields — `notifySelf` on todo, `copySelf` on lists —
27/// and there is no shared preferences service, so both are handled side by side.
28@Observable
29@MainActor
30final class NotificationPreferencesViewModel {
31
32 private(set) var notifySelf = false
33 private(set) var copySelf = false
34 private(set) var isLoading = false
35 private(set) var isSavingNotifySelf = false
36 private(set) var isSavingCopySelf = false
37 private(set) var hasLoaded = false
38 var error: String?
39
40 private let client: SRHTClient
41
42 init(client: SRHTClient) {
43 self.client = client
44 }
45
46 private static let todoPreferencesQuery = """
47 query todoPreferences {
48 preferences { notifySelf }
49 }
50 """
51
52 private static let listsPreferencesQuery = """
53 query listsPreferences {
54 preferences { copySelf }
55 }
56 """
57
58 private static let updateNotifySelfMutation = """
59 mutation updateTodoPreferences($notifySelf: Boolean!) {
60 preferences: updatePreferences(preferences: { notifySelf: $notifySelf }) {
61 notifySelf
62 }
63 }
64 """
65
66 private static let updateCopySelfMutation = """
67 mutation updateListsPreferences($copySelf: Boolean!) {
68 preferences: updatePreferences(preferences: { copySelf: $copySelf }) {
69 copySelf
70 }
71 }
72 """
73
74 func loadIfNeeded() async {
75 guard !hasLoaded, !isLoading else { return }
76 await load()
77 }
78
79 func load() async {
80 isLoading = true
81 error = nil
82 defer {
83 isLoading = false
84 hasLoaded = true
85 }
86
87 // The two services are independent; one being unreachable should not hide
88 // the other's setting.
89 async let todo = fetchNotifySelf()
90 async let lists = fetchCopySelf()
91
92 let (todoResult, listsResult) = await (todo, lists)
93
94 if let todoResult {
95 notifySelf = todoResult
96 }
97 if let listsResult {
98 copySelf = listsResult
99 }
100
101 if todoResult == nil && listsResult == nil {
102 error = "Couldn't load your email preferences."
103 }
104 }
105
106 /// The fetches stay in their own methods so the response types are only ever
107 /// decoded on the main actor. The module defaults to MainActor isolation, so
108 /// decoding straight from an `async let` would use a main-actor-isolated
109 /// Decodable conformance from a nonisolated context.
110 private func fetchNotifySelf() async -> Bool? {
111 let response = try? await client.execute(
112 service: .todo,
113 query: Self.todoPreferencesQuery,
114 responseType: TodoPreferencesResponse.self
115 )
116 return response?.preferences.notifySelf
117 }
118
119 private func fetchCopySelf() async -> Bool? {
120 let response = try? await client.execute(
121 service: .lists,
122 query: Self.listsPreferencesQuery,
123 responseType: ListsPreferencesResponse.self
124 )
125 return response?.preferences.copySelf
126 }
127
128 func setNotifySelf(_ newValue: Bool) async {
129 guard !isSavingNotifySelf else { return }
130 isSavingNotifySelf = true
131 error = nil
132 defer { isSavingNotifySelf = false }
133
134 let previous = notifySelf
135 notifySelf = newValue
136
137 do {
138 let response = try await client.execute(
139 service: .todo,
140 query: Self.updateNotifySelfMutation,
141 variables: ["notifySelf": newValue],
142 responseType: TodoPreferencesResponse.self
143 )
144 notifySelf = response.preferences.notifySelf
145 } catch {
146 notifySelf = previous
147 self.error = "Couldn't update ticket email preference. \(error.userFacingMessage)"
148 }
149 }
150
151 func setCopySelf(_ newValue: Bool) async {
152 guard !isSavingCopySelf else { return }
153 isSavingCopySelf = true
154 error = nil
155 defer { isSavingCopySelf = false }
156
157 let previous = copySelf
158 copySelf = newValue
159
160 do {
161 let response = try await client.execute(
162 service: .lists,
163 query: Self.updateCopySelfMutation,
164 variables: ["copySelf": newValue],
165 responseType: ListsPreferencesResponse.self
166 )
167 copySelf = response.preferences.copySelf
168 } catch {
169 copySelf = previous
170 self.error = "Couldn't update mailing list email preference. \(error.userFacingMessage)"
171 }
172 }
173}