gitbay/Views/Dashboard/DashboardView.swift

v1.1.1
gitbay-ios/gitbay/Views/Dashboard/DashboardView.swift history · blame · raw

184 lines · 6546 bytes

  1import SwiftUI
  2
  3/// The native representation of `gitbay dashboard`, ordered and phrased
  4/// like the logged-in web dashboard.
  5struct DashboardView: View {
  6
  7    @Environment(SessionStore.self) private var session
  8    @State private var model: DashboardViewModel
  9    @State private var createModel: RepoCreateViewModel
 10    @State private var composing = false
 11
 12    init(client: GitbayClient) {
 13        _model = State(initialValue: DashboardViewModel(client: client))
 14        _createModel = State(initialValue: RepoCreateViewModel(client: client))
 15    }
 16
 17    var body: some View {
 18        List {
 19            if let data = model.state.value {
 20                itemSection(
 21                    "Waiting on your review", items: data.reviewQueue,
 22                    empty: "Nothing waiting on you", marker: "!"
 23                ) { MRRoute.mr(repo: $0.repo, number: $0.number) }
 24
 25                itemSection(
 26                    "Assigned to you", items: data.assignedIssues,
 27                    empty: "Nothing assigned to you", marker: "#"
 28                ) { IssueRoute.issue(repo: $0.repo, number: $0.number) }
 29
 30                itemSection(
 31                    "Open merge requests", items: data.openMRs,
 32                    empty: "No open merge requests", marker: "!"
 33                ) { MRRoute.mr(repo: $0.repo, number: $0.number) }
 34
 35                itemSection(
 36                    "Open issues", items: data.openIssues,
 37                    empty: "No open issues", marker: "#"
 38                ) { IssueRoute.issue(repo: $0.repo, number: $0.number) }
 39
 40                pinnedSection(data.pinned)
 41                activitySection(data.recentActivity)
 42            }
 43        }
 44        .overlay { LoadStateOverlay(state: model.state) }
 45        .navigationTitle("Dashboard")
 46        .toolbar {
 47            ToolbarItem(placement: .topBarTrailing) {
 48                NavigationLink(value: RepoRoute.notifications) {
 49                    HStack(spacing: 2) {
 50                        Image(systemName: "bell")
 51                        if let unread = model.state.value?.unread, unread > 0 {
 52                            Text(String(unread))
 53                                .font(.gbSans(.caption2))
 54                        }
 55                    }
 56                }
 57                .accessibilityIdentifier("dashboard-notifications-button")
 58                .accessibilityLabel(bellLabel)
 59            }
 60            ToolbarItem(placement: .topBarTrailing) {
 61                Button {
 62                    composing = true
 63                } label: {
 64                    Image(systemName: "plus")
 65                }
 66                .accessibilityIdentifier("dashboard-create-button")
 67            }
 68        }
 69        .sheet(isPresented: $composing) {
 70            RepoCreateSheet(
 71                model: createModel,
 72                ownerPrefix: session.current?.username ?? ""
 73            ) {
 74                composing = false
 75                Task { await model.load() }
 76            }
 77        }
 78        .task { await model.load() }
 79        .refreshable { await model.load() }
 80    }
 81
 82    private var bellLabel: String {
 83        guard let unread = model.state.value?.unread, unread > 0 else { return "Notifications" }
 84        return "Notifications, \(unread) unread"
 85    }
 86
 87    private func itemSection(
 88        _ title: String,
 89        items: [DashboardItem],
 90        empty: String,
 91        marker: String,
 92        route: @escaping (DashboardItem) -> some Hashable
 93    ) -> some View {
 94        Section {
 95            if items.isEmpty {
 96                Text(empty)
 97                    .font(.gbSans(.subheadline))
 98                    .foregroundStyle(.secondary)
 99            } else {
100                ForEach(items) { item in
101                    NavigationLink(value: route(item)) {
102                        dashboardItem(item, marker: marker)
103                    }
104                }
105            }
106        } header: {
107            sectionHeader(title, count: items.count)
108        }
109    }
110
111    private func dashboardItem(_ item: DashboardItem, marker: String) -> some View {
112        VStack(alignment: .leading, spacing: 3) {
113            Text(item.title)
114                .font(.gbSans(.subheadline).weight(.semibold))
115                .lineLimit(2)
116            HStack(spacing: 4) {
117                Text(item.repo + marker + String(item.number))
118                    .font(.gbSans(.caption))
119                Text("·")
120                Text(item.author)
121                    .foregroundStyle(Color.gbAccent)
122                Text("·")
123                Text(item.updatedAt, format: .relative(presentation: .named))
124                if item.state == "source_gone" {
125                    Text("·")
126                    GBChip("source gone", .secondary)
127                }
128            }
129            .font(.gbSans(.caption))
130            .foregroundStyle(.secondary)
131            .lineLimit(1)
132        }
133        .padding(.vertical, 2)
134    }
135
136    private func pinnedSection(_ pinned: [RepoSummary]) -> some View {
137        Section {
138            if pinned.isEmpty {
139                Text("Pin a repository to keep it here")
140                    .font(.gbSans(.subheadline))
141                    .foregroundStyle(.secondary)
142            } else {
143                ForEach(pinned) { repo in
144                    NavigationLink(value: RepoRoute.repo(repo.path)) {
145                        HStack(spacing: 6) {
146                            Text(repo.path)
147                                .font(.gbSans(.subheadline).weight(.medium))
148                                .foregroundStyle(Color.gbAccent)
149                            if repo.isPrivate {
150                                GBChip("Private", .secondary)
151                            }
152                        }
153                    }
154                }
155            }
156        } header: {
157            Text("Pinned")
158        }
159    }
160
161    private func activitySection(_ events: [FeedEvent]) -> some View {
162        Section {
163            if events.isEmpty {
164                Text("No activity yet")
165                    .font(.gbSans(.subheadline))
166                    .foregroundStyle(.secondary)
167            } else {
168                ForEach(events) { event in
169                    FeedLink(event: event)
170                }
171            }
172        } header: {
173            Text("Recent activity")
174        }
175    }
176
177    private func sectionHeader(_ title: String, count: Int) -> some View {
178        HStack(alignment: .firstTextBaseline, spacing: 4) {
179            Text(title)
180            Text(String(count))
181                .foregroundStyle(.secondary)
182        }
183    }
184}