summaryrefslogtreecommitdiff
path: root/Hutch/Views/Lists
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-19 15:18:38 -0500
committerChristian Cleberg <[email protected]>2026-03-19 15:18:38 -0500
commit659c76df9ff3926cb68886c16167808c23bd8f75 (patch)
tree572bef546fcca19ad37bc1aa7cfb153eb2bf8eb7 /Hutch/Views/Lists
parented48e72520c51e4635cd3840dbc569bdd74c950f (diff)
parent6ba4e967d5dfb5d3c7bb97a0f2662f3180595563 (diff)
downloadhutch-2.tar.gz
hutch-2.tar.bz2
hutch-2.zip
v2.0: Merge branch 'dev'v2
Diffstat (limited to 'Hutch/Views/Lists')
-rw-r--r--Hutch/Views/Lists/MailingListListView.swift159
1 files changed, 159 insertions, 0 deletions
diff --git a/Hutch/Views/Lists/MailingListListView.swift b/Hutch/Views/Lists/MailingListListView.swift
new file mode 100644
index 0000000..80d4e50
--- /dev/null
+++ b/Hutch/Views/Lists/MailingListListView.swift
@@ -0,0 +1,159 @@
+import SwiftUI
+
+@Observable
+@MainActor
+final class MailingListListViewModel {
+ private(set) var mailingLists: [InboxMailingListReference] = []
+ private(set) var isLoading = false
+ var error: String?
+
+ private let client: SRHTClient
+
+ private static let subscriptionsQuery = """
+ query mailingLists($cursor: Cursor) {
+ subscriptions(cursor: $cursor) {
+ results {
+ ... on MailingListSubscription {
+ list {
+ id
+ rid
+ name
+ owner { canonicalName }
+ }
+ }
+ }
+ cursor
+ }
+ }
+ """
+
+ init(client: SRHTClient) {
+ self.client = client
+ }
+
+ func loadMailingLists() async {
+ guard !isLoading else { return }
+ isLoading = true
+ error = nil
+ defer { isLoading = false }
+
+ do {
+ mailingLists = try await fetchMailingLists()
+ } catch {
+ self.error = "Failed to load mailing lists"
+ }
+ }
+
+ private func fetchMailingLists() async throws -> [InboxMailingListReference] {
+ struct Response: Decodable, Sendable {
+ let subscriptions: Page
+ }
+
+ struct Page: Decodable, Sendable {
+ let results: [Subscription]
+ let cursor: String?
+ }
+
+ struct Subscription: Decodable, Sendable {
+ let list: InboxMailingListReference?
+ }
+
+ var results: [InboxMailingListReference] = []
+ var cursor: String?
+
+ while true {
+ var variables: [String: any Sendable] = [:]
+ if let cursor {
+ variables["cursor"] = cursor
+ }
+
+ let response = try await client.execute(
+ service: .lists,
+ query: Self.subscriptionsQuery,
+ variables: variables.isEmpty ? nil : variables,
+ responseType: Response.self
+ )
+
+ results.append(contentsOf: response.subscriptions.results.compactMap(\.list))
+ guard let nextCursor = response.subscriptions.cursor else {
+ break
+ }
+ cursor = nextCursor
+ }
+
+ var seen = Set<String>()
+ return results
+ .filter { seen.insert($0.rid).inserted }
+ .sorted {
+ if $0.owner.canonicalName == $1.owner.canonicalName {
+ return $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending
+ }
+ return $0.owner.canonicalName.localizedCaseInsensitiveCompare($1.owner.canonicalName) == .orderedAscending
+ }
+ }
+}
+
+struct MailingListListView: View {
+ @Environment(AppState.self) private var appState
+ @State private var viewModel: MailingListListViewModel?
+
+ var body: some View {
+ Group {
+ if let viewModel {
+ content(viewModel)
+ } else {
+ SRHTLoadingStateView(message: "Loading mailing lists…")
+ }
+ }
+ .navigationTitle("Lists")
+ .task {
+ if viewModel == nil {
+ let vm = MailingListListViewModel(client: appState.client)
+ viewModel = vm
+ await vm.loadMailingLists()
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func content(_ viewModel: MailingListListViewModel) -> some View {
+ @Bindable var vm = viewModel
+
+ List {
+ ForEach(viewModel.mailingLists, id: \.rid) { mailingList in
+ NavigationLink(value: MoreRoute.mailingList(mailingList)) {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(mailingList.name)
+ .font(.subheadline.weight(.medium))
+ Text(mailingList.owner.canonicalName)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ .padding(.vertical, 2)
+ }
+ }
+ }
+ .listStyle(.plain)
+ .overlay {
+ if viewModel.isLoading, viewModel.mailingLists.isEmpty {
+ SRHTLoadingStateView(message: "Loading mailing lists…")
+ } else if let error = viewModel.error, viewModel.mailingLists.isEmpty {
+ SRHTErrorStateView(
+ title: "Couldn't Load Mailing Lists",
+ message: error,
+ retryAction: { await viewModel.loadMailingLists() }
+ )
+ } else if viewModel.mailingLists.isEmpty {
+ ContentUnavailableView(
+ "No Mailing Lists",
+ systemImage: "list.bullet.rectangle",
+ description: Text("Your subscribed mailing lists will appear here.")
+ )
+ }
+ }
+ .srhtErrorBanner(error: $vm.error)
+ .refreshable {
+ await viewModel.loadMailingLists()
+ }
+ }
+}