gitbay/Views/Dashboard/DashboardView.swift

1c5622210d0f72efd86582ce195a6741401e7939
gitbay-ios/gitbay/Views/Dashboard/DashboardView.swift history · blame · raw

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}