Range-diff !169
back to !169 Reply by mail on and off
1: 5dad039 ! 1: ac96309 Reply by mail on and off
@@ Commit message
the account page shows it (reply_available, krz/gitbay#334); a server
that does not say offers it, and its refusal explains.
- A settings switch now shows the server's value once a write returns, so
- one the server refused goes back rather than showing what it refused.
- It used to keep the refused value, on every settings screen.
+ The account's switches go back when the server refuses a change rather
+ than showing what it refused: SettingToggle takes a write that says
+ whether it was accepted. Settings screens whose writes do not say keep
+ the reload's word, as before.
Closes #33
@@ gitbay/Account/AccountViewModel.swift: final class AccountViewModel {
+
+ /// `notifications settings reply on|off`: issue and merge request mail
+ /// carries a Reply-To, and a reply is posted as a comment.
-+ func setReply(_ on: Bool) async {
-+ await setPreference("reply", on)
++ @discardableResult
++ func setReply(_ on: Bool) async -> Bool {
++ return await setPreference("reply", on)
}
/// `notifications settings mail on|off`, then read the value back.
+- func setMail(_ on: Bool) async {
+- await setPreference("mail", on)
++ @discardableResult
++ func setMail(_ on: Bool) async -> Bool {
++ return await setPreference("mail", on)
+ }
+
+ /// `notifications settings watch on|off`, then read the value back.
+- func setWatch(_ on: Bool) async {
+- await setPreference("watch", on)
++ @discardableResult
++ func setWatch(_ on: Bool) async -> Bool {
++ return await setPreference("watch", on)
+ }
+
+- private func setPreference(_ name: String, _ on: Bool) async {
++ /// Whether the server took the change, so the switch can go back when
++ /// it did not.
++ private func setPreference(_ name: String, _ on: Bool) async -> Bool {
+ working = true
+ actionError = nil
+ defer { working = false }
+ do {
+ try await client.run(["notifications", "settings", name, on ? "on" : "off"])
+ await loadNotificationPreferences()
++ return true
+ } catch let error as GitbayError {
+ actionError = error.userFacingMessage
+ } catch {
+ actionError = GitbayError.transport(error).userFacingMessage
+ }
++ return false
+ }
+
+ // MARK: - SSH keys
## gitbay/Views/Account/AccountView.swift ##
@@ gitbay/Views/Account/AccountView.swift: struct AccountView: View {
- }
- .disabled(model.working)
- }
-+ if model.offersReply, let replyOn = model.replyOn {
-+ SettingToggle(title: "Reply by mail", serverValue: replyOn) {
-+ await model.setReply($0)
-+ }
+ private var notificationsSection: some View {
+ if let mailOn = model.mailOn {
+ Section {
+- SettingToggle(title: "Activity by mail", serverValue: mailOn) {
++ SettingToggle(title: "Activity by mail", serverValue: mailOn, accepted: {
+ await model.setMail($0)
+- }
++ })
+ .disabled(model.working)
+ if let watchOn = model.watchOn {
+- SettingToggle(title: "Watch writable repositories", serverValue: watchOn) {
++ SettingToggle(title: "Watch writable repositories", serverValue: watchOn, accepted: {
+ await model.setWatch($0)
+- }
++ })
+ .disabled(model.working)
+ }
++ if model.offersReply, let replyOn = model.replyOn {
++ SettingToggle(title: "Reply by mail", serverValue: replyOn, accepted: {
++ await model.setReply($0)
++ })
+ .disabled(model.working)
+ }
} header: {
Text("Notifications")
} footer: {
@@ gitbay/Views/Account/AccountView.swift: struct AccountView: View {
}
## gitbay/Views/Repos/RepoSettingsView.swift ##
-@@ gitbay/Views/Repos/RepoSettingsView.swift: struct SettingToggle: View {
+@@ gitbay/Views/Repos/RepoSettingsView.swift: private struct ApprovalsStepper: View {
+ /// A Toggle backed by real local state that mirrors the server value.
+ /// A computed Binding whose setter spawns a Task proved unreliable under
+ /// synthesized taps; a plain @State toggle is not.
++///
++/// A write that says whether the server took it lets the switch go back
++/// when it did not, rather than showing what was refused. A write that
++/// says nothing is taken as accepted, and the reload decides.
+ struct SettingToggle: View {
+
+ let title: String
let serverValue: Bool
- let write: (Bool) async -> Void
+- let write: (Bool) async -> Void
++ let write: (Bool) async -> Bool
-- @State private var isOn = false
-- @State private var seeded = false
-+ /// The value being written, shown until the write returns. Then the
-+ /// switch shows the server's value again: the new one when the write
-+ /// was accepted and the screen reloaded, the old one when it was
-+ /// refused. Computed as it is drawn, so it never races the reload.
-+ @State private var pending: Bool?
+ @State private var isOn = false
+ @State private var seeded = false
++ init(title: String, serverValue: Bool, write: @escaping (Bool) async -> Void) {
++ self.title = title
++ self.serverValue = serverValue
++ self.write = { await write($0); return true }
++ }
++
++ init(title: String, serverValue: Bool, accepted write: @escaping (Bool) async -> Bool) {
++ self.title = title
++ self.serverValue = serverValue
++ self.write = write
++ }
++
var body: some View {
-- Toggle(title, isOn: $isOn)
-- .onAppear {
-- if !seeded {
-- isOn = serverValue
-- seeded = true
-+ Toggle(title, isOn: Binding(
-+ get: { pending ?? serverValue },
-+ set: { newValue in
-+ guard pending == nil, newValue != serverValue else { return }
-+ pending = newValue
-+ Task {
-+ await write(newValue)
-+ pending = nil
- }
+ Toggle(title, isOn: $isOn)
+ .onAppear {
+@@ gitbay/Views/Repos/RepoSettingsView.swift: struct SettingToggle: View {
}
-- .onChange(of: serverValue) { _, newValue in
-- isOn = newValue
-- }
-- .onChange(of: isOn) { _, newValue in
-- guard newValue != serverValue else { return }
+ .onChange(of: isOn) { _, newValue in
+ guard newValue != serverValue else { return }
- Task { await write(newValue) }
-- }
-+ ))
++ Task {
++ if !(await write(newValue)) { isOn = !newValue }
++ }
+ }
}
}