gitbay/Views/Dashboard/DashboardView.swift
167 lines · 5784 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 Button {
49 composing = true
50 } label: {
51 Image(systemName: "plus")
52 }
53 .accessibilityIdentifier("dashboard-create-button")
54 }
55 AccountMenu()
56 }
57 .sheet(isPresented: $composing) {
58 RepoCreateSheet(
59 model: createModel,
60 ownerPrefix: session.current?.username ?? ""
61 ) {
62 composing = false
63 Task { await model.load() }
64 }
65 }
66 .task { await model.load() }
67 .refreshable { await model.load() }
68 }
69
70 private func itemSection(
71 _ title: String,
72 items: [DashboardItem],
73 empty: String,
74 marker: String,
75 route: @escaping (DashboardItem) -> some Hashable
76 ) -> some View {
77 Section {
78 if items.isEmpty {
79 Text(empty)
80 .font(.gbSans(.subheadline))
81 .foregroundStyle(.secondary)
82 } else {
83 ForEach(items) { item in
84 NavigationLink(value: route(item)) {
85 dashboardItem(item, marker: marker)
86 }
87 }
88 }
89 } header: {
90 sectionHeader(title, count: items.count)
91 }
92 }
93
94 private func dashboardItem(_ item: DashboardItem, marker: String) -> some View {
95 VStack(alignment: .leading, spacing: 3) {
96 Text(item.title)
97 .font(.gbSans(.subheadline).weight(.semibold))
98 .lineLimit(2)
99 HStack(spacing: 4) {
100 Text(item.repo + marker + String(item.number))
101 .font(.gbSans(.caption))
102 Text("·")
103 Text(item.author)
104 .foregroundStyle(Color.gbAccent)
105 Text("·")
106 Text(item.updatedAt, format: .relative(presentation: .named))
107 if item.state == "source_gone" {
108 Text("·")
109 GBChip("source gone", .secondary)
110 }
111 }
112 .font(.gbSans(.caption))
113 .foregroundStyle(.secondary)
114 .lineLimit(1)
115 }
116 .padding(.vertical, 2)
117 }
118
119 private func pinnedSection(_ pinned: [RepoSummary]) -> some View {
120 Section {
121 if pinned.isEmpty {
122 Text("Pin a repository to keep it here")
123 .font(.gbSans(.subheadline))
124 .foregroundStyle(.secondary)
125 } else {
126 ForEach(pinned) { repo in
127 NavigationLink(value: RepoRoute.repo(repo.path)) {
128 HStack(spacing: 6) {
129 Text(repo.path)
130 .font(.gbSans(.subheadline).weight(.medium))
131 .foregroundStyle(Color.gbAccent)
132 if repo.isPrivate {
133 GBChip("Private", .secondary)
134 }
135 }
136 }
137 }
138 }
139 } header: {
140 Text("Pinned")
141 }
142 }
143
144 private func activitySection(_ events: [FeedEvent]) -> some View {
145 Section {
146 if events.isEmpty {
147 Text("No activity yet")
148 .font(.gbSans(.subheadline))
149 .foregroundStyle(.secondary)
150 } else {
151 ForEach(events) { event in
152 FeedLink(event: event)
153 }
154 }
155 } header: {
156 Text("Recent activity")
157 }
158 }
159
160 private func sectionHeader(_ title: String, count: Int) -> some View {
161 HStack(alignment: .firstTextBaseline, spacing: 4) {
162 Text(title)
163 Text(String(count))
164 .foregroundStyle(.secondary)
165 }
166 }
167}