krz/rune
an ios client for njalla
clone: git clone https://gitbay.org/krz/rune.git
v1.2.0: Rune/Views/Domains/ForwardListView.swift · raw
1import SwiftUI
2
3struct ForwardListView: View {
4 let domainName: String
5 @ObservedObject var viewModel: DomainViewModel
6 let client: NjallaClient
7
8 @State private var showingAddForward = false
9 @State private var forwardPendingDeletion: EmailForward?
10
11 var body: some View {
12 List {
13 if let errorMessage = viewModel.forwardsErrorMessage {
14 Section {
15 InlineErrorView(message: errorMessage, retryTitle: "Retry Forwards") {
16 Task {
17 await viewModel.loadForwards(for: domainName, client: client)
18 }
19 }
20 .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
21 }
22 }
23
24 if viewModel.isLoadingForwards && viewModel.forwards.isEmpty {
25 Section {
26 HStack {
27 Spacer()
28 ProgressView("Loading Forwards")
29 Spacer()
30 }
31 }
32 } else if viewModel.forwards.isEmpty {
33 Section {
34 ContentUnavailableView(
35 "No Forwards",
36 systemImage: "envelope",
37 description: Text("No email forwards are configured for this domain.")
38 )
39 }
40 } else {
41 ForEach(viewModel.forwards) { forward in
42 VStack(alignment: .leading, spacing: 4) {
43 Text("@\(forward.from)")
44 .font(.headline)
45 Text(forward.to)
46 .font(.subheadline)
47 .foregroundStyle(.secondary)
48 }
49 .padding(.vertical, 4)
50 .swipeActions {
51 Button("Delete", role: .destructive) {
52 guard !viewModel.isSaving else { return }
53 forwardPendingDeletion = forward
54 }
55 }
56 .contextMenu {
57 Button("Delete Forward", role: .destructive) {
58 forwardPendingDeletion = forward
59 }
60 }
61 .disabled(viewModel.isSaving)
62 }
63 }
64 }
65 .listStyle(.insetGrouped)
66 .navigationTitle("Forwards")
67 .navigationBarTitleDisplayMode(.inline)
68 .toolbar {
69 Button {
70 showingAddForward = true
71 } label: {
72 Label("Add Forward", systemImage: "plus")
73 }
74 .disabled(viewModel.isSaving)
75 }
76 .sheet(isPresented: $showingAddForward) {
77 NavigationStack {
78 ForwardAddView(domainName: domainName, viewModel: viewModel, client: client)
79 }
80 }
81 .task {
82 debugLog("Loading forwards for \(domainName)")
83 await viewModel.loadForwards(for: domainName, client: client)
84 debugLog("Loaded \(viewModel.forwards.count) forwards for \(domainName)")
85 }
86 .refreshable {
87 debugLog("Refreshing forwards for \(domainName)")
88 await viewModel.loadForwards(for: domainName, client: client)
89 debugLog("Refresh complete with \(viewModel.forwards.count) forwards for \(domainName)")
90 }
91 .alert(deleteAlertTitle, isPresented: deleteBinding) {
92 Button("Delete Forward", role: .destructive) {
93 guard let forwardPendingDeletion else { return }
94 Task {
95 await delete(forwardPendingDeletion)
96 }
97 }
98 Button("Cancel", role: .cancel) {
99 forwardPendingDeletion = nil
100 }
101 } message: {
102 Text("Delete the forward from \(forwardPendingDeletion?.from ?? "") to \(forwardPendingDeletion?.to ?? "")?")
103 }
104 .alert("Request Failed", isPresented: mutationErrorBinding) {
105 Button("OK", role: .cancel) {}
106 } message: {
107 Text(viewModel.mutationErrorMessage ?? "")
108 }
109 }
110
111 private func delete(_ forward: EmailForward) async {
112 guard !viewModel.isSaving else {
113 return
114 }
115
116 debugLog("Deleting forward \(forward.from)@\(forward.domain) -> \(forward.to)")
117 do {
118 try await viewModel.removeForward(forward, client: client)
119 debugLog("Deleted forward \(forward.from)@\(forward.domain) -> \(forward.to)")
120 forwardPendingDeletion = nil
121 } catch is CancellationError {
122 debugLog("Delete cancelled for \(forward.from)@\(forward.domain) -> \(forward.to)")
123 return
124 } catch {
125 debugLog("Delete failed for \(forward.from)@\(forward.domain) -> \(forward.to): \(error.localizedDescription)")
126 return
127 }
128 }
129
130 private var deleteAlertTitle: String {
131 guard let forwardPendingDeletion else {
132 return ""
133 }
134
135 return "Delete forward \(forwardPendingDeletion.from)@\(domainName)?"
136 }
137
138 private var deleteBinding: Binding<Bool> {
139 Binding(
140 get: { forwardPendingDeletion != nil },
141 set: { newValue in
142 if !newValue {
143 forwardPendingDeletion = nil
144 }
145 }
146 )
147 }
148
149 private var mutationErrorBinding: Binding<Bool> {
150 Binding(
151 get: { viewModel.mutationErrorMessage != nil },
152 set: { newValue in
153 if !newValue {
154 viewModel.dismissMutationError()
155 }
156 }
157 )
158 }
159
160 private func debugLog(_ message: String) {
161 #if DEBUG
162 debugPrint("[ForwardListView]", message)
163 #endif
164 }
165}