import SwiftUI /// The native representation of `gitbay dashboard`, ordered and phrased /// like the logged-in web dashboard. struct DashboardView: View { @Environment(SessionStore.self) private var session @State private var model: DashboardViewModel @State private var createModel: RepoCreateViewModel @State private var composing = false init(client: GitbayClient) { _model = State(initialValue: DashboardViewModel(client: client)) _createModel = State(initialValue: RepoCreateViewModel(client: client)) } var body: some View { List { if let data = model.state.value { itemSection( "Waiting on your review", items: data.reviewQueue, empty: "Nothing waiting on you", marker: "!" ) { MRRoute.mr(repo: $0.repo, number: $0.number) } itemSection( "Assigned to you", items: data.assignedIssues, empty: "Nothing assigned to you", marker: "#" ) { IssueRoute.issue(repo: $0.repo, number: $0.number) } itemSection( "Open merge requests", items: data.openMRs, empty: "No open merge requests", marker: "!" ) { MRRoute.mr(repo: $0.repo, number: $0.number) } itemSection( "Open issues", items: data.openIssues, empty: "No open issues", marker: "#" ) { IssueRoute.issue(repo: $0.repo, number: $0.number) } pinnedSection(data.pinned) activitySection(data.recentActivity) } } .overlay { LoadStateOverlay(state: model.state) } .navigationTitle("Dashboard") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { composing = true } label: { Image(systemName: "plus") } .accessibilityIdentifier("dashboard-create-button") } } .sheet(isPresented: $composing) { RepoCreateSheet( model: createModel, ownerPrefix: session.current?.username ?? "" ) { composing = false Task { await model.load() } } } .task { await model.load() } .refreshable { await model.load() } } private func itemSection( _ title: String, items: [DashboardItem], empty: String, marker: String, route: @escaping (DashboardItem) -> some Hashable ) -> some View { Section { if items.isEmpty { Text(empty) .font(.gbSans(.subheadline)) .foregroundStyle(.secondary) } else { ForEach(items) { item in NavigationLink(value: route(item)) { dashboardItem(item, marker: marker) } } } } header: { sectionHeader(title, count: items.count) } } private func dashboardItem(_ item: DashboardItem, marker: String) -> some View { VStack(alignment: .leading, spacing: 3) { Text(item.title) .font(.gbSans(.subheadline).weight(.semibold)) .lineLimit(2) HStack(spacing: 4) { Text(item.repo + marker + String(item.number)) .font(.gbSans(.caption)) Text("·") Text(item.author) .foregroundStyle(Color.gbAccent) Text("·") Text(item.updatedAt, format: .relative(presentation: .named)) if item.state == "source_gone" { Text("·") GBChip("source gone", .secondary) } } .font(.gbSans(.caption)) .foregroundStyle(.secondary) .lineLimit(1) } .padding(.vertical, 2) } private func pinnedSection(_ pinned: [RepoSummary]) -> some View { Section { if pinned.isEmpty { Text("Pin a repository to keep it here") .font(.gbSans(.subheadline)) .foregroundStyle(.secondary) } else { ForEach(pinned) { repo in NavigationLink(value: RepoRoute.repo(repo.path)) { HStack(spacing: 6) { Text(repo.path) .font(.gbSans(.subheadline).weight(.medium)) .foregroundStyle(Color.gbAccent) if repo.isPrivate { GBChip("Private", .secondary) } } } } } } header: { Text("Pinned") } } private func activitySection(_ events: [FeedEvent]) -> some View { Section { if events.isEmpty { Text("No activity yet") .font(.gbSans(.subheadline)) .foregroundStyle(.secondary) } else { ForEach(events) { event in FeedLink(event: event) } } } header: { Text("Recent activity") } } private func sectionHeader(_ title: String, count: Int) -> some View { HStack(alignment: .firstTextBaseline, spacing: 4) { Text(title) Text(String(count)) .foregroundStyle(.secondary) } } }