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}