krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v2: Hutch/Views/Lists/MailingListListView.swift · raw

  1import SwiftUI
  2
  3@Observable
  4@MainActor
  5final class MailingListListViewModel {
  6    private(set) var mailingLists: [InboxMailingListReference] = []
  7    private(set) var isLoading = false
  8    var error: String?
  9
 10    private let client: SRHTClient
 11
 12    private static let subscriptionsQuery = """
 13    query mailingLists($cursor: Cursor) {
 14        subscriptions(cursor: $cursor) {
 15            results {
 16                ... on MailingListSubscription {
 17                    list {
 18                        id
 19                        rid
 20                        name
 21                        owner { canonicalName }
 22                    }
 23                }
 24            }
 25            cursor
 26        }
 27    }
 28    """
 29
 30    init(client: SRHTClient) {
 31        self.client = client
 32    }
 33
 34    func loadMailingLists() async {
 35        guard !isLoading else { return }
 36        isLoading = true
 37        error = nil
 38        defer { isLoading = false }
 39
 40        do {
 41            mailingLists = try await fetchMailingLists()
 42        } catch {
 43            self.error = "Failed to load mailing lists"
 44        }
 45    }
 46
 47    private func fetchMailingLists() async throws -> [InboxMailingListReference] {
 48        struct Response: Decodable, Sendable {
 49            let subscriptions: Page
 50        }
 51
 52        struct Page: Decodable, Sendable {
 53            let results: [Subscription]
 54            let cursor: String?
 55        }
 56
 57        struct Subscription: Decodable, Sendable {
 58            let list: InboxMailingListReference?
 59        }
 60
 61        var results: [InboxMailingListReference] = []
 62        var cursor: String?
 63
 64        while true {
 65            var variables: [String: any Sendable] = [:]
 66            if let cursor {
 67                variables["cursor"] = cursor
 68            }
 69
 70            let response = try await client.execute(
 71                service: .lists,
 72                query: Self.subscriptionsQuery,
 73                variables: variables.isEmpty ? nil : variables,
 74                responseType: Response.self
 75            )
 76
 77            results.append(contentsOf: response.subscriptions.results.compactMap(\.list))
 78            guard let nextCursor = response.subscriptions.cursor else {
 79                break
 80            }
 81            cursor = nextCursor
 82        }
 83
 84        var seen = Set<String>()
 85        return results
 86            .filter { seen.insert($0.rid).inserted }
 87            .sorted {
 88                if $0.owner.canonicalName == $1.owner.canonicalName {
 89                    return $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending
 90                }
 91                return $0.owner.canonicalName.localizedCaseInsensitiveCompare($1.owner.canonicalName) == .orderedAscending
 92            }
 93    }
 94}
 95
 96struct MailingListListView: View {
 97    @Environment(AppState.self) private var appState
 98    @State private var viewModel: MailingListListViewModel?
 99
100    var body: some View {
101        Group {
102            if let viewModel {
103                content(viewModel)
104            } else {
105                SRHTLoadingStateView(message: "Loading mailing lists…")
106            }
107        }
108        .navigationTitle("Lists")
109        .task {
110            if viewModel == nil {
111                let vm = MailingListListViewModel(client: appState.client)
112                viewModel = vm
113                await vm.loadMailingLists()
114            }
115        }
116    }
117
118    @ViewBuilder
119    private func content(_ viewModel: MailingListListViewModel) -> some View {
120        @Bindable var vm = viewModel
121
122        List {
123            ForEach(viewModel.mailingLists, id: \.rid) { mailingList in
124                NavigationLink(value: MoreRoute.mailingList(mailingList)) {
125                    VStack(alignment: .leading, spacing: 4) {
126                        Text(mailingList.name)
127                            .font(.subheadline.weight(.medium))
128                        Text(mailingList.owner.canonicalName)
129                            .font(.caption)
130                            .foregroundStyle(.secondary)
131                    }
132                    .padding(.vertical, 2)
133                }
134            }
135        }
136        .listStyle(.plain)
137        .overlay {
138            if viewModel.isLoading, viewModel.mailingLists.isEmpty {
139                SRHTLoadingStateView(message: "Loading mailing lists…")
140            } else if let error = viewModel.error, viewModel.mailingLists.isEmpty {
141                SRHTErrorStateView(
142                    title: "Couldn't Load Mailing Lists",
143                    message: error,
144                    retryAction: { await viewModel.loadMailingLists() }
145                )
146            } else if viewModel.mailingLists.isEmpty {
147                ContentUnavailableView(
148                    "No Mailing Lists",
149                    systemImage: "list.bullet.rectangle",
150                    description: Text("Your subscribed mailing lists will appear here.")
151                )
152            }
153        }
154        .srhtErrorBanner(error: $vm.error)
155        .refreshable {
156            await viewModel.loadMailingLists()
157        }
158    }
159}